├── .env.testing ├── .github ├── admin-error-notice.png ├── coverage.svg ├── links-in-logs.png ├── logs-wp-list-table.png ├── plugins-page-logs-link.png ├── test-plugin.png └── woocommerce-settings.png ├── .gitignore ├── .htaccess ├── CHANGELOG.md ├── LICENCE ├── README.md ├── assets ├── bh-wp-logger-admin.js ├── bh-wp-logger.css └── vendor │ ├── colresizable │ ├── colResizable-1.6.js │ └── colResizable-1.6.min.js │ └── renderjson │ └── renderjson.js ├── codeception.dist.yml ├── composer.json ├── composer.lock ├── patchwork.json ├── phpcs.woocommerce.xml ├── phpcs.xml ├── phpstan-bootstrap.php ├── phpstan.neon ├── src ├── admin │ ├── class-admin-notices.php │ ├── class-ajax.php │ ├── class-logs-list-table.php │ ├── class-logs-page.php │ ├── class-plugin-installer.php │ └── class-plugins-page.php ├── api │ ├── class-api.php │ └── class-bh-wp-psr-logger.php ├── class-logger.php ├── interface-api-interface.php ├── interface-logger-settings-interface.php ├── interface-woocommerce-logger-settings-interface.php ├── php │ ├── class-php-error-handler.php │ └── class-php-shutdown-handler.php ├── private-uploads │ └── class-url-is-public.php ├── trait-logger-settings-trait.php └── wp-includes │ ├── class-cli.php │ ├── class-cron.php │ ├── class-functions.php │ ├── class-init.php │ └── class-plugin-logger-actions.php ├── test-plugin ├── Admin │ ├── class-admin-ajax.php │ ├── class-admin.php │ ├── css │ │ └── bh-wp-logger-test-plugin-admin.css │ ├── js │ │ └── bh-wp-logger-test-plugin-admin.js │ └── partials │ │ └── bh-wp-logger-test-plugin-admin-display.php ├── Includes │ ├── class-bh-wp-logger-test-plugin.php │ └── class-i18n.php ├── LICENSE.txt ├── README.txt ├── bh-wp-logger-test-plugin.php ├── languages │ └── bh-wp-logger-test-plugin.pot └── uninstall.php ├── tests ├── _data │ ├── .gitkeep │ └── dump.sql ├── _output │ └── .gitignore ├── _support │ ├── AcceptanceTester.php │ ├── FunctionalTester.php │ ├── Helper │ │ ├── Acceptance.php │ │ ├── Functional.php │ │ ├── Unit.php │ │ └── Wpunit.php │ ├── UnitTester.php │ ├── WpunitTester.php │ └── _generated │ │ └── .gitignore ├── bootstrap.php ├── integration.suite.yml ├── integration │ ├── _bootstrap.php │ ├── api │ │ └── class-bh-wp-psr-logger-integration-Test.php │ ├── class-bh-wp-logger-develop-Test.php │ └── wp-includes │ │ ├── class-bh-wp-logger-integration-Test.php │ │ ├── class-functions-integration-Test.php │ │ └── class-i18n-Test.php ├── unit.suite.yml ├── unit │ ├── _bootstrap.php │ ├── admin │ │ ├── class-admin-notices-unit-Test.php │ │ ├── class-ajax-unit-Test.php │ │ ├── class-logs-page-unit-Test.php │ │ ├── class-plugin-installer-unit-Test.php │ │ └── class-plugins-page-unit-Test.php │ ├── api │ │ ├── class-api-unit-Test.php │ │ └── class-bh-wp-psr-logger-unit-Test.php │ ├── class-logger-unit-Test.php │ ├── class-plugin-unit-Test.php │ ├── patchwork.json │ ├── php │ │ ├── class-php-error-handler-unit-Test.php │ │ └── class-php-shutdown-handler-unit-Test.php │ ├── private-uploads │ │ └── class-url-is-public-unit-Test.php │ └── wp-includes │ │ ├── class-cron-unit-Test.php │ │ ├── class-init-unit-Test.php │ │ └── class-plugin-logger-actions-unit-Test.php ├── wpunit.suite.yml └── wpunit │ ├── _bootstrap.php │ ├── admin │ ├── class-admin-notices-wpunit-Test.php │ └── class-log-list-table-wpunit-Test.php │ ├── api │ └── class-bh-wp-psr-logger-wpunit-Test.php │ ├── class-logger-settings-trait-wpunit-Test.php │ ├── class-logger-wpunit-Test.php │ ├── php │ └── class-php-error-handler-wpunit-Test.php │ └── wp-includes │ ├── class-functions-wpunit-Test.php │ └── class-i18n-wpunit-Test.php └── wp-cli.yml /.env.testing: -------------------------------------------------------------------------------- 1 | PLUGIN_NAME="BH WP Logger Test Plugin" 2 | PLUGIN_SLUG=bh-wp-logger-test-plugin 3 | WP_ROOT_FOLDER="wordpress" 4 | TEST_SITE_WP_ADMIN_PATH="/wp-admin" 5 | TEST_SITE_DB_NAME="bh_wp_logger_tests" 6 | TEST_SITE_DB_HOST="127.0.0.1" 7 | TEST_SITE_DB_USER="bh-wp-logger" 8 | TEST_SITE_DB_PASSWORD="bh-wp-logger" 9 | TEST_SITE_TABLE_PREFIX="wp_" 10 | TEST_DB_NAME="bh_wp_logger_integration" 11 | TEST_DB_HOST="127.0.0.1" 12 | TEST_DB_USER="bh-wp-logger" 13 | TEST_DB_PASSWORD="bh-wp-logger" 14 | TEST_TABLE_PREFIX="wp_" 15 | TEST_SITE_WP_URL="http://localhost:8080/bh-wp-logger/" 16 | TEST_SITE_WP_DOMAIN="localhost:8080" 17 | TEST_SITE_ADMIN_EMAIL="email@example.org" 18 | TEST_SITE_ADMIN_USERNAME="admin" 19 | TEST_SITE_ADMIN_PASSWORD="password" 20 | -------------------------------------------------------------------------------- /.github/admin-error-notice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianHenryIE/bh-wp-logger/0a4b6247f42b3717147d9f5db55b7e62799514af/.github/admin-error-notice.png -------------------------------------------------------------------------------- /.github/coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PHPUnit 17 | PHPUnit 18 | 43% 19 | 43% 20 | 21 | -------------------------------------------------------------------------------- /.github/links-in-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianHenryIE/bh-wp-logger/0a4b6247f42b3717147d9f5db55b7e62799514af/.github/links-in-logs.png -------------------------------------------------------------------------------- /.github/logs-wp-list-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianHenryIE/bh-wp-logger/0a4b6247f42b3717147d9f5db55b7e62799514af/.github/logs-wp-list-table.png -------------------------------------------------------------------------------- /.github/plugins-page-logs-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianHenryIE/bh-wp-logger/0a4b6247f42b3717147d9f5db55b7e62799514af/.github/plugins-page-logs-link.png -------------------------------------------------------------------------------- /.github/test-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianHenryIE/bh-wp-logger/0a4b6247f42b3717147d9f5db55b7e62799514af/.github/test-plugin.png -------------------------------------------------------------------------------- /.github/woocommerce-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianHenryIE/bh-wp-logger/0a4b6247f42b3717147d9f5db55b7e62799514af/.github/woocommerce-settings.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | Thumbs.db 16 | .cache 17 | .project 18 | .settings 19 | .tmproj 20 | *.esproj 21 | nbproject 22 | *.sublime-project 23 | *.sublime-workspace 24 | 25 | # Dreamweaver added files 26 | _notes 27 | dwsync.xml 28 | 29 | # Komodo 30 | *.komodoproject 31 | .komodotools 32 | 33 | # Folders to ignore 34 | .hg 35 | .svn 36 | .CVS 37 | intermediate 38 | .idea 39 | cache 40 | 41 | /vendor/ 42 | 43 | wordpress 44 | test-plugin/vendor/ 45 | wp-content/ 46 | 47 | 48 | scratch 49 | 50 | # Generated from GitHub Secrets in the workflows. 51 | .env.secret 52 | auth.json 53 | 54 | *.zip 55 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_URI} !bh-wp-logger/wordpress/ [NC] 3 | RewriteCond %{REQUEST_URI} !/vendor-prefixed/ [NC] 4 | RewriteCond %{REQUEST_URI} !/vendor/ [NC] 5 | RewriteCond %{REQUEST_URI} !/assets/ [NC] 6 | RewriteCond %{REQUEST_URI} !/templates/ [NC] 7 | RewriteRule (.*) wordpress/$1 [L] 8 | 9 | 10 | 11 | # Enable WP_DEBUG. 12 | #php_flag log_errors 1 13 | #php_value error_log "wp-content/php_errors.log" 14 | 15 | # WP_DEBUG_DISPLAY 16 | # ini_set( 'display_errors', 1 ); 17 | #php_flag display_errors 1 # not working. 18 | #SetEnv display_errors=1 19 | 20 | #SetEnv WP_ENVIRONMENT_TYPE=local 21 | 22 | #php_value WP_DEBUG_DISPLAY true 23 | 24 | #php_flag display_errors 1 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | == 0.4 == 4 | 5 | * Fix: fatal error in brianhenryie/bh-wp-private-uploads 6 | * Fix: fatal error when using `Logger_Settings_Trait` default plugin name 7 | * Add: WP CLI command to log context 8 | * Add: WP CLI commands to delete logs 9 | 10 | == 0.3 == 2023-04-11 == 11 | 12 | * Use new WPTT Admin Notices bugfix patch https://github.com/WPTT/admin-notices/pull/15 13 | * Fix: JavaScript dependencies (`wp_enqueue_script` `$deps` array) 14 | * Fix: Move `alleyinteractive/wordpress-autoloader` to `autoload-dev` 15 | * Do not add actions and filters when log level is none (temp fix, needs finesse) 16 | * Removed `Logger_Settings` and `Plugins` classes in favour of much improved/simplified `Logger_Settings_Trait` to infer defaults (WIP) 17 | * Performance: Conditionally add WordPress `doing_it_wrong`, `deprecated_function`, etc. logging 18 | * Performance: Cache all backtraces, and share caches between all `bh-wp-logger` instances 19 | * Add: WordPress `doing_it_wrong`, `deprecated_function` etc. test buttons in test-plugin 20 | * Improved WPCS, PhpStan 21 | 22 | 74 PhpUnit tests, ~48% coverage. 23 | 24 | == 0.2 == 2023-03-02 == 25 | 26 | * Fix: Test plugin loading assets 27 | * Add: auto-size date column, allow resizing all columns 28 | * Add: Format context JSON with show/hide controls 29 | * Add: Checkboxes to filter rows by log level 30 | 31 | https://github.com/caldwell/renderjson 32 | 33 | https://github.com/alvaro-prieto/colResizable 34 | 35 | -------------------------------------------------------------------------------- /assets/bh-wp-logger-admin.js: -------------------------------------------------------------------------------- 1 | (function( $ ) { 2 | 'use strict'; 3 | 4 | $(function() { 5 | 6 | function show_hide_log_checkbox( element ) { 7 | let id = $(element).attr('id'); 8 | let log_level = id.replace('log_level_display_checkbox_',''); 9 | let css_class = 'tr.level-'+log_level; 10 | let display = $(element).is(':checked') ? '' : 'none'; 11 | 12 | $(css_class).css('display', display); 13 | 14 | // TODO: Refresh the page height. Scrolling to the bottom currently results in lots of empty whitespace after the rows are hidden. 15 | } 16 | 17 | $('.log_level_display_checkbox').click( function( e ) { 18 | show_hide_log_checkbox( this ); 19 | }); 20 | 21 | // Respect the state upon refresh. 22 | $('.log_level_display_checkbox').each( function () { 23 | show_hide_log_checkbox( this ); 24 | }); 25 | 26 | // When the date is changed, reload the page. 27 | $('#log_date').change(function() { 28 | var selectedDate = $('#log_date').val(); 29 | 30 | var urlParams = new URLSearchParams(window.location.search); 31 | urlParams.set('log_date',selectedDate); 32 | 33 | window.location = location.pathname+'?' + urlParams ; 34 | }); 35 | 36 | // When the delete button is clicked... 37 | $( '.button.logs-page' ).on( 38 | 'click', 39 | function(event) { 40 | event.preventDefault(); 41 | 42 | let buttonName = event.target.name; 43 | 44 | var data = {}; 45 | 46 | // $_GET['page'] has the slug. 47 | // e.g. ?page=bh-wp-logger-test-plugin-logs 48 | var urlParams = new URLSearchParams(window.location.search); 49 | let slug_log = urlParams.get('page'); 50 | if( false === slug_log.endsWith('-logs') ) { 51 | return; 52 | } 53 | let slug = slug_log.slice(0, -5); 54 | 55 | data.plugin_slug = slug; 56 | data._wpnonce = $('#delete_logs_wpnonce').val(); 57 | 58 | switch ( buttonName) { 59 | case 'deleteButton': 60 | data.action = 'bh_wp_logger_logs_delete'; 61 | // let deleteButton = document.getElementById( 'deleteButton' ).data; 62 | // let dateToDelete = deleteButton.dataset.date; 63 | let dateToDelete = event.target.dataset.date; 64 | data.date_to_delete = dateToDelete; 65 | break; 66 | case 'deleteAllButton': 67 | data.action = 'bh_wp_logger_logs_delete_all'; 68 | break; 69 | default: 70 | return; 71 | } 72 | 73 | $.post( 74 | ajaxurl, 75 | data, 76 | function (response) { 77 | var urlParams = new URLSearchParams(window.location.search); 78 | // TODO: it should change to the closest date. 79 | urlParams.delete('log_date'); 80 | window.location = location.pathname+'?' + urlParams ; 81 | 82 | } 83 | ); 84 | 85 | } 86 | ); 87 | 88 | // Set the column-time width to only what's needed. 89 | $('.column-time').css('width',$('td.column-time').first().children().first().width()+25); 90 | 91 | $('.wp-list-table').colResizable(); 92 | 93 | renderjson.set_icons('+', '-'); 94 | renderjson.set_show_to_level(2); 95 | 96 | $('.log-context-pre').each( function () { 97 | 98 | let json_context = $(this).data('json'); 99 | let rendered_context = renderjson(json_context); 100 | 101 | $(this).parent().append(rendered_context); 102 | 103 | // Leave the existing content in the TD so it can be searched. 104 | $(this).css('height','0'); 105 | $(this).css('font-size','0'); 106 | }); 107 | 108 | } 109 | ); 110 | 111 | })( jQuery ); 112 | -------------------------------------------------------------------------------- /assets/bh-wp-logger.css: -------------------------------------------------------------------------------- 1 | 2 | /* Logs styles */ 3 | 4 | /* The th */ 5 | #level { 6 | width: 10px; 7 | padding: 0; 8 | } 9 | 10 | 11 | /* the td */ 12 | .level.column-level { 13 | padding: 0; 14 | border-right: 1px solid #c3c4c7; 15 | } 16 | 17 | /* without inline-block, the padding only applies to the first line of the span. */ 18 | .logs-time { 19 | padding-left: 5px; 20 | display: inline-block; 21 | } 22 | 23 | 24 | .level-error .level.column-level { 25 | background-color: #dc3232; 26 | } 27 | 28 | .level-warning .level.column-level { 29 | background-color: #ffb900; 30 | } 31 | 32 | .level-notice .level.column-level { 33 | background-color: #00a0d2; 34 | } 35 | 36 | .level-info .level.column-level { 37 | background-color: #FFF; 38 | } 39 | 40 | .level-debug .level.column-level { 41 | background-color: #ccd0d4; 42 | } 43 | 44 | .log-context-pre { 45 | overflow-x: scroll; 46 | } 47 | 48 | 49 | .renderjson a { text-decoration: none; } 50 | .renderjson .disclosure { color: crimson; 51 | font-size: 150%; } 52 | .renderjson .syntax { color: grey; } 53 | .renderjson .string { color: red; } 54 | .renderjson .number { color: cyan; } 55 | .renderjson .boolean { color: plum; } 56 | .renderjson .key { color: lightblue; } 57 | .renderjson .keyword { color: lightgoldenrodyellow; } 58 | .renderjson .object.syntax { color: lightseagreen; } 59 | .renderjson .array.syntax { color: lightsalmon; } 60 | 61 | .renderjson { 62 | overflow-x: scroll; 63 | margin-top: 0; 64 | margin-bottom: 0; 65 | } -------------------------------------------------------------------------------- /assets/vendor/colresizable/colResizable-1.6.min.js: -------------------------------------------------------------------------------- 1 | !function(t){Array.indexOf||(Array.prototype.indexOf=function(t){for(var e=0;e0;r.append("");var p=function(t){var e=t.attr("id");(t=s[e])&&t.is("table")&&(t.removeClass(n+" "+l).gc.remove(),delete s[e])},g=function(i){var r=i.find(">thead>tr:first>th,>thead>tr:first>td");r.length||(r=i.find(">tbody>tr:first>th,>tr:first>th,>tbody>tr:first>td, >tr:first>td")),r=r.filter(":visible"),i.cg=i.find("col"),i.ln=r.length,i.p&&e&&e[i.id]&&u(i,r),r.each((function(e){var r=t(this),o=-1!=i.dc.indexOf(e),s=t(i.gc.append('
')[0].lastChild);s.append(o?"":i.opt.gripInnerHtml).append('
'),e==i.ln-1&&(s.addClass("JCLRLastGrip"),i.f&&s.html("")),s.on("touchstart mousedown",x),o?s.addClass("JCLRdisabledGrip"):s.removeClass("JCLRdisabledGrip").on("touchstart mousedown",x),s.t=i,s.i=e,s.c=r,r.w=r.width(),i.g.push(s),i.c.push(r),r.width(r.w).removeAttr("width"),s.data(n,{i:e,t:i.attr("id"),last:e==i.ln-1})})),i.cg.removeAttr("width"),i.find("td, th").not(r).not("table th, table td").each((function(){t(this).removeAttr("width")})),i.f||i.removeAttr("width").addClass(l),w(i)},u=function(t,i){var r,o,s=0,d=0,n=[];if(i){if(t.cg.removeAttr("width"),t.opt.flush)return void(e[t.id]="");for(o=(r=e[t.id].split(";"))[t.ln+1],!t.f&&o&&(t.width(o*=1),t.opt.overflow&&(t.css("min-width",o+a),t.w=o));d*{cursor:"+a.opt.dragCursor+"!important}"),l.addClass(a.opt.draggingClass),o=l,a.c[d.i].l)for(var f,h=0;h.JCLRgrip .JColResizer:hover{cursor:"+a.opt.hoverCursor+"!important}"),a.addClass(n).attr("id",l).before('
'),a.g=[],a.c=[],a.w=a.width(),a.gc=a.prev(),a.f=a.opt.fixed,o.marginLeft&&a.gc.css("marginLeft",o.marginLeft),o.marginRight&&a.gc.css("marginRight",o.marginRight),a.cs=c(h?i.cellSpacing||i.currentStyle.borderSpacing:a.css("border-spacing"))||2,a.b=c(h?i.border||i.currentStyle.borderLeftWidth:a.css("border-left-width"))||1,s[l]=a,g(a))}(this,i)}))}})}(jQuery); -------------------------------------------------------------------------------- /codeception.dist.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: tests 3 | output: tests/_output 4 | data: tests/_data 5 | support: tests/_support 6 | envs: tests/_envs 7 | actor_suffix: Tester 8 | extensions: 9 | enabled: 10 | - Codeception\Extension\RunFailed 11 | commands: 12 | - Codeception\Command\GenerateWPUnit 13 | - Codeception\Command\GenerateWPRestApi 14 | - Codeception\Command\GenerateWPRestController 15 | - Codeception\Command\GenerateWPRestPostTypeController 16 | - Codeception\Command\GenerateWPAjax 17 | - Codeception\Command\GenerateWPCanonical 18 | - Codeception\Command\GenerateWPXMLRPC 19 | params: 20 | - .env.testing 21 | coverage: 22 | enabled: true 23 | include: 24 | - src/* 25 | exclude: 26 | - src/dependencies/* 27 | - /*/interface*.php 28 | - src/vendor/* 29 | - /*/index.php 30 | - /*/*.txt 31 | - src/autoload.php 32 | - /*/*.css 33 | - /*/*.js 34 | bootstrap: bootstrap.php 35 | -------------------------------------------------------------------------------- /patchwork.json: -------------------------------------------------------------------------------- 1 | { 2 | "redefinable-internals": [ 3 | "realpath" 4 | ] 5 | } -------------------------------------------------------------------------------- /phpcs.woocommerce.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WooCommerce extension PHP_CodeSniffer ruleset. 4 | 5 | 6 | . 7 | 8 | 9 | tests/ 10 | test-plugin/ 11 | */node_modules/* 12 | /dist-archive/ 13 | /scratch/ 14 | /vendor/* 15 | /wordpress/ 16 | /wp-content/ 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | tests/ 48 | 49 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins 4 | 5 | 6 | 7 | 8 | 9 | . 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | /assets/ 18 | /scratch/ 19 | /src/strauss/* 20 | /tests/_data/ 21 | /tests/_output/ 22 | /tests/_support/ 23 | /vendor/* 24 | /wordpress/ 25 | /wp-content/ 26 | 27 | 28 | 29 | */tests/* 30 | 31 | 32 | */tests/* 33 | 34 | 35 | */tests/* 36 | 37 | 38 | */tests/* 39 | 40 | 41 | */tests/* 42 | 43 | 44 | */tests/acceptance/* 45 | 46 | 47 | */tests/* 48 | 49 | 50 | */tests/* 51 | 52 | 53 | */tests/* 54 | 55 | 56 | */tests/* 57 | 58 | 59 | */tests/* 60 | 61 | 62 | */tests/* 63 | 64 | 65 | */tests/* 66 | 67 | 68 | */tests/* 69 | 70 | 71 | */tests/acceptance/* 72 | 73 | 74 | */tests/acceptance/* 75 | 76 | 77 | */tests/* 78 | 79 | 80 | */tests/* 81 | 82 | 83 | */tests/* 84 | 85 | 86 | */tests/* 87 | 88 | 89 | */tests/* 90 | 91 | 92 | */tests/* 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /phpstan-bootstrap.php: -------------------------------------------------------------------------------- 1 | setLogger( $logger ?? new NullLogger() ); 40 | $this->settings = $settings; 41 | $this->api = $api; 42 | } 43 | 44 | protected function get_error_detail_option_name(): string { 45 | return $this->settings->get_plugin_slug() . '-recent-error-data'; 46 | } 47 | 48 | /** 49 | * The last error is stored in the option `plugin-slug-recent-error-data` as an array with `message` and `timestamp`. 50 | * 51 | * @see Admin_Notices::get_error_detail_option_name() 52 | * 53 | * @return ?array{message: string, timestamp: string} 54 | */ 55 | protected function get_last_error(): ?array { 56 | $last_error = get_option( $this->get_error_detail_option_name(), null ); 57 | return $last_error; 58 | } 59 | 60 | /** 61 | * Show a notice for recent errors in the logs. 62 | * 63 | * TODO: Do not show on plugin install page. 64 | * TODO: Check file exists before linking to it. 65 | * 66 | * hooked earlier than 10 because Notices::boot() also hooks a function on admin_init that needs to run after this. 67 | * 68 | * @hooked admin_init 69 | */ 70 | public function admin_notices(): void { 71 | 72 | // We don't need to register the admin notice except to display it and to handle the dismiss button. 73 | if ( ! is_admin() && ! wp_doing_ajax() ) { 74 | return; 75 | } 76 | 77 | // TODO: alwasy return on updater.php 78 | 79 | $error_detail_option_name = $this->get_error_detail_option_name(); 80 | 81 | // If we're on the logs page, don't show the admin notice linking to the logs page. 82 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended 83 | if ( isset( $_GET['page'] ) && $this->settings->get_plugin_slug() . '-logs' === sanitize_key( $_GET['page'] ) ) { 84 | delete_option( $error_detail_option_name ); 85 | return; 86 | } 87 | 88 | $last_error = $this->get_last_error(); 89 | 90 | $last_log_time = $this->api->get_last_log_time(); 91 | $last_logs_view_time = $this->api->get_last_logs_view_time(); 92 | 93 | // TODO: This should be comparing $last_error time? 94 | if ( ! empty( $last_error ) && ( is_null( $last_logs_view_time ) || $last_log_time > $last_logs_view_time ) ) { 95 | 96 | $is_dismissed_option_name = "wptrt_notice_dismissed_{$this->settings->get_plugin_slug()}-recent-error"; 97 | 98 | // wptrt_notice_dismissed_bh-wp-logger-test-plugin-recent-error 99 | 100 | $error_text = isset( $last_error['message'] ) ? trim( $last_error['message'] ) : ''; 101 | $error_time = isset( $last_error['timestamp'] ) ? (int) $last_error['timestamp'] : ''; 102 | 103 | $title = ''; 104 | $content = "{$this->settings->get_plugin_name()}. Error: "; 105 | 106 | if ( ! empty( $error_text ) ) { 107 | $content .= "\"{$error_text}\" "; 108 | } 109 | 110 | if ( ! empty( $error_time ) && is_numeric( $error_time ) ) { 111 | $content .= ' at ' . gmdate( 'Y-m-d\TH:i:s\Z', $error_time ) . ' UTC.'; 112 | 113 | // wp_timezone(); 114 | 115 | // Link to logs. 116 | $log_link = $this->api->get_log_url( gmdate( 'Y-m-d', $error_time ) ); 117 | 118 | } else { 119 | $log_link = $this->api->get_log_url(); 120 | } 121 | 122 | if ( ! empty( $log_link ) ) { 123 | $content .= ' View Logs.

'; 124 | } 125 | 126 | // ID must be globally unique because it is the css id that will be used. 127 | $this->add( 128 | $this->settings->get_plugin_slug() . '-recent-error', 129 | $title, // The title for this notice. 130 | $content, // The content for this notice. 131 | array( 132 | 'scope' => 'global', 133 | 'type' => 'error', 134 | ) 135 | ); 136 | 137 | /** 138 | * When the notice is dismissed, delete the error detail option (to stop the notice being recreated), 139 | * and delete the saved dismissed flag (which would prevent it displaying when the next error occurs). 140 | * 141 | * @see update_option() 142 | */ 143 | $on_dismiss = function ( $value, $old_value, $option ) use ( $error_detail_option_name ) { 144 | delete_option( $error_detail_option_name ); 145 | delete_option( $option ); 146 | return $old_value; // When new and old match, it short circuits. 147 | }; 148 | add_filter( "pre_update_option_{$is_dismissed_option_name}", $on_dismiss, 10, 3 ); 149 | 150 | // wptrt_notice_dismissed_bh-wp-logger-test-plugin-recent-error 151 | 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/admin/class-ajax.php: -------------------------------------------------------------------------------- 1 | api = $api; 45 | $this->settings = $settings; 46 | } 47 | 48 | /** 49 | * Delete a single log file. 50 | * 51 | * Request body should contain: 52 | * * `action` "bh_wp_logger_logs_delete_all". 53 | * * `date_to_delete` in `Y-m-d` format, e.g. 2022-03-02. 54 | * * `plugin_slug` containing the slug used in settings. 55 | * * `_wpnonce` with action `bh-wp-logger-delete`. 56 | * 57 | * Response format will be: 58 | * array{success: bool, message: ?string}. 59 | * 60 | * @hooked wp_ajax_bh_wp_logger_logs_delete 61 | * 62 | * @uses API_Interface::delete_log() 63 | */ 64 | public function delete(): void { 65 | 66 | if ( ! isset( $_POST['_wpnonce'], $_POST['plugin_slug'], $_POST['date_to_delete'] ) 67 | || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'bh-wp-logger-delete' ) ) { 68 | return; 69 | } 70 | 71 | // bh-wp-logger could be hooked for many plugins. 72 | if ( $this->settings->get_plugin_slug() !== sanitize_key( $_POST['plugin_slug'] ) ) { 73 | return; 74 | } 75 | 76 | $ymd_date = sanitize_key( $_POST['date_to_delete'] ); 77 | 78 | $result = $this->api->delete_log( $ymd_date ); 79 | 80 | if ( $result['success'] ) { 81 | wp_send_json_success( $result ); 82 | } else { 83 | wp_send_json_error( $result ); 84 | } 85 | } 86 | 87 | /** 88 | * Delete all log files for this plugin. 89 | * 90 | * Request body should contain: 91 | * * `action` "bh_wp_logger_logs_delete". 92 | * * `plugin_slug` containing the slug used in settings. 93 | * * `_wpnonce` with action "bh-wp-logger-delete". 94 | * 95 | * Response format will be: 96 | * array{success: bool, message: ?string}. 97 | * 98 | * @hooked wp_ajax_bh_wp_logger_logs_delete_all 99 | * 100 | * @uses \BrianHenryIE\WP_Logger\API_Interface::delete_all_logs() 101 | */ 102 | public function delete_all(): void { 103 | 104 | if ( ! isset( $_POST['_wpnonce'], $_POST['plugin_slug'] ) 105 | || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'bh-wp-logger-delete' ) ) { 106 | return; 107 | } 108 | 109 | // bh-wp-logger could be hooked for many plugins. 110 | if ( $this->settings->get_plugin_slug() !== sanitize_key( $_POST['plugin_slug'] ) ) { 111 | return; 112 | } 113 | 114 | $result = $this->api->delete_all_logs(); 115 | 116 | if ( $result['success'] ) { 117 | wp_send_json_success( $result ); 118 | } else { 119 | wp_send_json_error( $result ); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/admin/class-plugin-installer.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 33 | } 34 | 35 | /** 36 | * Add the Logs page link to the existing links. 37 | * 38 | * @hooked install_plugin_complete_actions 39 | * @see \Plugin_Installer_Skin::after() 40 | * 41 | * @param string[] $install_actions Array of plugin action links. 42 | * @param object $_api Object containing WordPress.org API plugin data. Empty 43 | * for non-API installs, such as when a plugin is installed 44 | * via upload. 45 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 46 | * 47 | * @return string[] 48 | */ 49 | public function add_logs_link( $install_actions, $_api, $plugin_file ): array { 50 | 51 | if ( $plugin_file !== $this->settings->get_plugin_basename() ) { 52 | return $install_actions; 53 | } 54 | 55 | $install_actions[] = '•'; 56 | 57 | $logs_url = admin_url( '/admin.php?page=' . $this->settings->get_plugin_slug() . '-logs' ); 58 | $install_actions[] = 'Go to ' . esc_html( $this->settings->get_plugin_name() ) . ' logs'; 59 | 60 | return $install_actions; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/admin/class-plugins-page.php: -------------------------------------------------------------------------------- 1 | if there are unviewed logs. 7 | * 8 | * e.g. /wp-admin/admin.php?page=bh-wp-logger-test-plugin-logs. 9 | * 10 | * @package brianhenryie/bh-wp-logger 11 | */ 12 | 13 | namespace BrianHenryIE\WP_Logger\Admin; 14 | 15 | use BrianHenryIE\WP_Logger\API_Interface; 16 | use BrianHenryIE\WP_Logger\Logger_Settings_Interface; 17 | use Psr\Log\LoggerAwareTrait; 18 | use Psr\Log\LoggerInterface; 19 | use Psr\Log\NullLogger; 20 | 21 | /** 22 | * Class Plugins_Page 23 | */ 24 | class Plugins_Page { 25 | 26 | use LoggerAwareTrait; 27 | 28 | /** 29 | * Needed for the plugin slug. 30 | * 31 | * @var Logger_Settings_Interface 32 | */ 33 | protected Logger_Settings_Interface $settings; 34 | 35 | /** 36 | * Needed for the log file path. 37 | * 38 | * @var API_Interface 39 | */ 40 | protected $api; 41 | 42 | /** 43 | * Plugins_Page constructor. 44 | * 45 | * @param API_Interface $api The logger's main functions. 46 | * @param Logger_Settings_Interface $settings The logger settings. 47 | * @param ?LoggerInterface $logger The logger itself. 48 | */ 49 | public function __construct( API_Interface $api, Logger_Settings_Interface $settings, ?LoggerInterface $logger = null ) { 50 | 51 | $this->setLogger( $logger ?? new NullLogger() ); 52 | $this->settings = $settings; 53 | $this->api = $api; 54 | } 55 | 56 | /** 57 | * Adds 'Logs' link to under the plugin name on plugins.php. 58 | * Attempts to place it immediately before the deactivate link. 59 | * 60 | * @hooked plugin_action_links_{plugin basename} 61 | * @see \WP_Plugins_List_Table::display_rows() 62 | * 63 | * @param array $action_links The existing plugin links (usually "Deactivate"). 64 | * @param string $_plugin_basename The plugin's directory/filename.php. 65 | * @param array $_plugin_data Associative array including PluginURI, slug, Author, Version. See `get_plugin_data()`. 66 | * @param string $_context The plugin context. By default this can include 'all', 'active', 'inactive', 67 | * 'recently_activated', 'upgrade', 'mustuse', 'dropins', and 'search'. 68 | * 69 | * @return array The links to display below the plugin name on plugins.php. 70 | */ 71 | public function add_logs_action_link( array $action_links, string $_plugin_basename, $_plugin_data, $_context ): array { 72 | 73 | // Presumably the deactivate link. 74 | // When a plugin is "required" it does not have a deactivate link. 75 | if ( count( $action_links ) > 0 ) { 76 | $deactivate_link = array_pop( $action_links ); 77 | } 78 | 79 | $logs_link = $this->api->get_log_url(); 80 | 81 | $last_log_time = $this->api->get_last_log_time(); 82 | $last_logs_view_time = $this->api->get_last_logs_view_time(); 83 | 84 | if ( ! is_null( $last_log_time ) && ! is_null( $last_logs_view_time ) 85 | && $last_log_time > $last_logs_view_time ) { 86 | $action_links[] = '' . __( 'Logs', 'bh-wp-logger' ) . ''; 87 | } else { 88 | $action_links[] = '' . __( 'Logs', 'bh-wp-logger' ) . ''; 89 | } 90 | 91 | if ( isset( $deactivate_link ) ) { 92 | $action_links[] = $deactivate_link; 93 | } 94 | 95 | return $action_links; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/api/class-bh-wp-psr-logger.php: -------------------------------------------------------------------------------- 1 | cli_logger = ( defined( 'WP_CLI' ) && WP_CLI 42 | && class_exists( WP_CLI_Logger::class ) ) 43 | ? new WP_CLI_Logger() 44 | : new NullLogger(); 45 | } 46 | 47 | /** 48 | * Return the true (proxied) logger. 49 | */ 50 | public function get_logger(): LoggerInterface { 51 | return $this->logger; 52 | } 53 | 54 | /** 55 | * When an error is being logged, record the time of the last error, so later, an admin notice can be displayed, 56 | * to inform them of the new problem. 57 | * 58 | * TODO: This always displays the admin notice even when the log itself is filtered. i.e. this function runs before 59 | * the filter, so the code needs to be moved. 60 | * TODO: Allow configuring which log levels result in the admin notice. 61 | * 62 | * TODO: include a link to the log url so the last file with an error will be linked, rather than the most recent log file. 63 | * 64 | * @param string $message The message to be logged. 65 | * @param array $context Data to record the system state at the time of the log. 66 | */ 67 | public function error( $message, $context = array() ) { 68 | 69 | $this->log( LogLevel::ERROR, $message, $context ); 70 | } 71 | 72 | 73 | /** 74 | * The last function in this plugin before the actual logging is delegated to KLogger/WC_Logger... 75 | * * If WP_CLI is available, log to console. 76 | * * If logger is not available (presumably WC_Logger not yet initialized), enqueue the log to retry on plugins_loaded. 77 | * * Set WC_Logger 'source'. 78 | * * Execute the actual logging command. 79 | * * Record in wp_options the time of the last log. 80 | * 81 | * TODO: Add a filter on level. 82 | * 83 | * @see LogLevel 84 | * 85 | * @param string $level The log severity. 86 | * @param string $message The message to log. 87 | * @param array $context Additional information to be logged (not saved at all log levels). 88 | */ 89 | public function log( $level, $message, $context = array() ) { 90 | 91 | $context = array_merge( $context, $this->get_common_context() ); 92 | 93 | if ( isset( $context['exception'] ) && $context['exception'] instanceof \Throwable ) { 94 | $exception_backtrace = $context['exception']->getTrace(); 95 | // Backtrace::createForThrowable( $exception_backtrace ); 96 | 97 | } 98 | 99 | $settings_log_level = $this->settings->get_log_level(); 100 | 101 | if ( LogLevel::ERROR === $level ) { 102 | 103 | $debug_backtrace = $this->get_backtrace( null, null ); 104 | $context['debug_backtrace'] = $debug_backtrace; 105 | 106 | // TODO: This could be useful on all logs. 107 | global $wp_current_filter; 108 | $context['filters'] = $wp_current_filter; 109 | 110 | } elseif ( LogLevel::WARNING === $level || LogLevel::DEBUG === $settings_log_level ) { 111 | 112 | $debug_backtrace = $this->get_backtrace( null, 3 ); 113 | $context['debug_backtrace'] = $debug_backtrace; 114 | 115 | global $wp_current_filter; 116 | $context['filters'] = $wp_current_filter; 117 | } 118 | 119 | if ( isset( $context['exception'] ) && $context['exception'] instanceof \Exception ) { 120 | $exception = $context['exception']; 121 | $exception_details = array(); 122 | $exception_details['class'] = get_class( $exception ); 123 | $exception_details['message'] = $exception->getMessage(); 124 | 125 | $reflect = new \ReflectionClass( get_class( $exception ) ); 126 | $props = array(); 127 | foreach ( $reflect->getProperties() as $property ) { 128 | $property->setAccessible( true ); 129 | $props[ $property->getName() ] = $property->getValue( $exception ); 130 | } 131 | $exception_details['properties'] = $props; 132 | 133 | $context['exception'] = $exception_details; 134 | } 135 | 136 | /** 137 | * TODO: regex to replace email addresses with b**********e@gmail.com, credit card numbers etc. 138 | * There's a PHP proposal for omitting info from logs. 139 | * 140 | * @see https://wiki.php.net/rfc/redact_parameters_in_back_traces 141 | */ 142 | 143 | $log_data = array( 144 | 'level' => $level, 145 | 'message' => $message, 146 | 'context' => $context, 147 | ); 148 | $settings = $this->settings; 149 | $bh_wp_psr_logger = $this; 150 | 151 | /** 152 | * Filter to modify the log data. 153 | * Return null to cancel logging this message. 154 | * 155 | * @param array{level:string,message:string,context:array} $log_data 156 | * @param Logger_Settings_Interface $settings 157 | * @param BH_WP_PSR_Logger $bh_wp_psr_logger 158 | */ 159 | $log_data = apply_filters( $this->settings->get_plugin_slug() . '_bh_wp_logger_log', $log_data, $settings, $bh_wp_psr_logger ); 160 | 161 | if ( empty( $log_data ) ) { 162 | return; 163 | } 164 | 165 | list( $level, $message, $context ) = array_values( $log_data ); 166 | 167 | if ( LogLevel::ERROR === $level ) { 168 | update_option( 169 | $this->settings->get_plugin_slug() . '-recent-error-data', 170 | array( 171 | 'message' => $message, 172 | 'timestamp' => time(), 173 | ) 174 | ); 175 | } 176 | 177 | // Add WP CLI command to context. 178 | if(defined('WP_CLI') && constant('WP_CLI')) { 179 | $context['wp_cli'] = array( 180 | array( 181 | 'command' => 'wp ' . implode( ' ', WP_CLI::get_runner()->arguments ), 182 | 'assoc_args' => WP_CLI::get_runner()->assoc_args, 183 | ) 184 | ); 185 | } 186 | 187 | $this->logger->$level( $message, $context ); 188 | $this->cli_logger->$level( $message, $context ); 189 | 190 | // We store the last log time in a transient to avoid reading the file from disk. When a new log is written, 191 | // that transient is expired. TODO: We're deleting here on the assumption deleting is more performant than writing 192 | // the new value. This could also be run only in WordPress's 'shutdown' action. 193 | delete_transient( $this->get_last_log_time_transient_name() ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/class-logger.php: -------------------------------------------------------------------------------- 1 | get_log_level() ) { 65 | return new NullLogger(); 66 | } 67 | 68 | $logger = new self( $settings ); 69 | 70 | self::$instance = $logger; 71 | 72 | // Add the hooks. 73 | new Plugin_Logger_Actions( self::$instance, $settings, self::$instance ); 74 | } 75 | 76 | return self::$instance; 77 | } 78 | 79 | /** 80 | * If log level is 'none', use NullLogger. 81 | * If Settings is WooCommerce_Logger_Settings_Interface use WC_Logger. 82 | * Otherwise use KLogger. 83 | * 84 | * @param Logger_Settings_Interface $settings Basic settings required for the logger. 85 | */ 86 | public function __construct( Logger_Settings_Interface $settings ) { 87 | 88 | if ( $settings instanceof WooCommerce_Logger_Settings_Interface 89 | && in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins', array() ) ), true ) ) { 90 | // Does not use `is_plugin_active()` here because "Call to undefined function" error (maybe an admin function). 91 | 92 | $logger = new WC_PSR_Logger( $settings ); 93 | 94 | // Add context to WooCommerce logs. 95 | $wc_log_handler = new Log_Context_Handler( $settings ); 96 | add_filter( 'woocommerce_format_log_entry', array( $wc_log_handler, 'add_context_to_logs' ), 10, 2 ); 97 | 98 | // TODO: What's the log file name when it's a wc-log? 99 | 100 | } else { 101 | 102 | $log_directory = wp_normalize_path( WP_CONTENT_DIR . '/uploads/logs' ); 103 | $log_level_threshold = $settings->get_log_level(); 104 | 105 | /** 106 | * Add the `{context}` template string, 107 | * then provide `'appendContext' => false` to Klogger (since it is already takes care of). 108 | * 109 | * @see \Katzgrau\KLogger\Logger::formatMessage() 110 | */ 111 | $log_format = "{date} {level} {message}\n{context}"; 112 | 113 | /** 114 | * `c` is chosen to match WooCommerce's choice. 115 | * 116 | * @see WC_Log_Handler::format_time() 117 | */ 118 | $options = array( 119 | 'extension' => 'log', 120 | 'prefix' => "{$settings->get_plugin_slug()}-", 121 | 'dateFormat' => 'c', 122 | 'logFormat' => $log_format, 123 | 'appendContext' => false, 124 | ); 125 | 126 | $logger = new KLogger( $log_directory, $log_level_threshold, $options ); 127 | 128 | // Make the logs directory inaccessible to the public. 129 | $private_uploads_settings = new class( $settings ) implements Private_Uploads_Settings_Interface { 130 | use Private_Uploads_Settings_Trait; 131 | 132 | /** 133 | * The settings provided for the logger. We need the plugin slug as a uid for the private uploads instance. 134 | * 135 | * @var Logger_Settings_Interface 136 | */ 137 | protected Logger_Settings_Interface $logger_settings; 138 | 139 | /** 140 | * Constructor. 141 | * 142 | * @param Logger_Settings_Interface $logger_settings The plugin logger settings, whose plugin slug we need. 143 | */ 144 | public function __construct( Logger_Settings_Interface $logger_settings ) { 145 | $this->logger_settings = $logger_settings; 146 | } 147 | 148 | /** 149 | * This is used as a unique id for the Private Uploads instance. 150 | */ 151 | public function get_plugin_slug(): string { 152 | return $this->logger_settings->get_plugin_slug() . '_logger'; 153 | } 154 | 155 | /** 156 | * Use wp-content/uploads/logs as the logs directory. 157 | */ 158 | public function get_uploads_subdirectory_name(): string { 159 | return 'logs'; 160 | } 161 | }; 162 | 163 | // Don't use the Private_Uploads singleton in case the parent plugin also needs it. 164 | $private_uploads = new Private_Uploads( $private_uploads_settings, $this ); 165 | new BH_WP_Private_Uploads_Hooks( $private_uploads, $private_uploads_settings, $this ); 166 | 167 | } 168 | 169 | parent::__construct( $settings, $logger ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/interface-api-interface.php: -------------------------------------------------------------------------------- 1 | Y-m-d index with path as the value. 30 | */ 31 | public function get_log_files( ?string $date = null ): array; 32 | 33 | /** 34 | * Get the URL for the settings page Logs link. 35 | * 36 | * @param ?string $date A date string Y-m-d or null to get the most recent. 37 | * 38 | * @return string 39 | */ 40 | public function get_log_url( ?string $date = null ): string; 41 | 42 | /** 43 | * Delete the log file for a specific date. 44 | * 45 | * @param string $ymd_date The date formatted Y-m-d, e.g. 2021-09-27. 46 | * 47 | * @used-by Logs_Page 48 | * 49 | * @return array{success: bool, message: ?string} 50 | */ 51 | public function delete_log( string $ymd_date ): array; 52 | 53 | /** 54 | * Delete all logs for this plugin. 55 | * 56 | * @used-by Logs_Page 57 | * @used-by AJAX::delete_all() 58 | * 59 | * @return array{success: bool, deleted_files: string[], failed_to_delete: string[]} 60 | */ 61 | public function delete_all_logs(): array; 62 | 63 | /** 64 | * Delete old log files, so they don't build up. 65 | * Presumably called from cron. 66 | * 67 | * @used-by Cron 68 | */ 69 | public function delete_old_logs(): void; 70 | 71 | /** 72 | * Set the context that will be added to all log messages. 73 | * e.g. request_id, user_id. 74 | * 75 | * This will be cumulative, but reset on every request (every new page load). 76 | * 77 | * @param string $key The entry's key to save. This will overwrite any existing entry. 78 | * @param mixed $value The new value. 79 | */ 80 | public function set_common_context( string $key, $value ): void; 81 | 82 | /** 83 | * Get context to add to each log message. 84 | * e.g. session, filters used, IP. 85 | * 86 | * @return array 87 | */ 88 | public function get_common_context(): array; 89 | 90 | /** 91 | * Get the debug backtrace leading up to the point the message was logged. 92 | * 93 | * @param ?int $steps The number of entries to return. 94 | * 95 | * @return Frame[] 96 | */ 97 | public function get_backtrace( ?string $source_hash = null, ?int $steps = null ): array; 98 | 99 | /** 100 | * Checks the current backtrace for any reference to the current plugin. 101 | * 102 | * Use `implode( '', func_get_args() )` 103 | */ 104 | public function is_backtrace_contains_plugin( ?string $source_hash = null ): bool; 105 | 106 | /** 107 | * Given a filepath (from a backtrace or error message) determine is it from this plugin. 108 | * 109 | * @param string $filepath Filesystem filepath to check. 110 | * 111 | * @return bool 112 | */ 113 | public function is_file_from_plugin( string $filepath ): bool; 114 | 115 | /** 116 | * Reads the most recent log file, if any, and returns the time of the most recent log (or null). 117 | * 118 | * @used-by Plugins_Page::add_logs_action_link() 119 | * 120 | * @return ?DateTimeInterface 121 | */ 122 | public function get_last_log_time(): ?DateTimeInterface; 123 | 124 | /** 125 | * Find the time the logs were last viewed. 126 | * 127 | * @used-by Plugins_Page::add_logs_action_link() 128 | * 129 | * @return ?DateTimeInterface 130 | */ 131 | public function get_last_logs_view_time(): ?DateTimeInterface; 132 | 133 | /** 134 | * Record the last time the logs were viewed in order to determine if admin notices should or should not be displayed. 135 | * i.e. "mark read". 136 | * 137 | * @used-by Logs_Page 138 | * 139 | * @param ?DateTimeInterface $date_time A time to set, defaults to "now". 140 | */ 141 | public function set_last_logs_view_time( ?DateTimeInterface $date_time = null ): void; 142 | 143 | /** 144 | * Given the path to a log text file, parse its lines into an array of individual log entries parsed into 145 | * time, level, message, and context. 146 | * 147 | * @used-by Logs_List_Table::get_data() 148 | * 149 | * @param string $filepath The full path to the log file. 150 | * 151 | * @return array 152 | */ 153 | public function parse_log( string $filepath ): array; 154 | } 155 | -------------------------------------------------------------------------------- /src/interface-logger-settings-interface.php: -------------------------------------------------------------------------------- 1 | setLogger( $logger ); 58 | $this->api = $api; 59 | $this->settings = $settings; 60 | } 61 | 62 | /** 63 | * Since this is hooked on plugins_loaded, it won't display errors that occur as plugins' constructors run. 64 | * 65 | * But since it's hooked on plugins_loaded (as distinct from just initializing), it allows other plugins to 66 | * hook error handlers in before or after this one. 67 | * 68 | * @hooked plugins_loaded 69 | */ 70 | public function init(): void { 71 | 72 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler 73 | $this->previous_error_handler = set_error_handler( 74 | array( $this, 'plugin_error_handler' ), 75 | E_ALL 76 | ); 77 | } 78 | 79 | /** 80 | * The error handler itself. 81 | * 82 | * @see set_error_handler() 83 | * 84 | * @param int $errno The error code (the level of the error raised, as an integer). 85 | * @param string $errstr A string describing the error. 86 | * @param string $errfile The filename in which the error occurred. 87 | * @param int $errline The line number in which the error occurred. 88 | * 89 | * @return bool True if error had been handled and no more handling to do, false to pass the error on. 90 | */ 91 | public function plugin_error_handler( int $errno, string $errstr, string $errfile, int $errline ) { 92 | 93 | $func_args = func_get_args(); 94 | 95 | $plugin_related_error = $this->is_related_error( $errno, $errstr, $errfile, $errline ); 96 | 97 | if ( ! $plugin_related_error ) { 98 | // If there is another handler, return its result, otherwise indicate the error was not handled. 99 | return $this->return_error_handler_result( false, $func_args ); 100 | } 101 | 102 | // e.g. my-plugin-slug-logged-error-4f6ead5467acd... 103 | $transient_key = "{$this->settings->get_plugin_slug()}-logged-{$this->errno_to_psr3( $errno )}-" . md5( $errstr ); 104 | 105 | $transient_value = get_transient( $transient_key ); 106 | 107 | // We've already logged this error recently, don't bother logging it again. 108 | if ( ! empty( $transient_value ) ) { 109 | return $this->return_error_handler_result( true, $func_args ); 110 | } 111 | 112 | // func_get_args shows extra parameters: `["queue_conn":false,"oauth2_refresh":false]`. 113 | $context = array(); 114 | $context['error'] = array_combine( array( 'errno', 'errstr', 'errfile', 'errline' ), array_slice( $func_args, 0, 4 ) ); 115 | 116 | $backtrace_cache_hash = sanitize_key( implode( ',', array_slice( $func_args, 0, 4 ) ) ); 117 | 118 | // This would be added anyway in some cases, but for PHP errors, let's always have a little backtrace. 119 | $backtrace_frames = $this->api->get_backtrace( $backtrace_cache_hash, 3 ); 120 | $context['debug_backtrace'] = $backtrace_frames; 121 | 122 | $log_level = $this->errno_to_psr3( $errno ); 123 | 124 | $this->logger->$log_level( $errstr, $context ); 125 | 126 | set_transient( $transient_key, wp_json_encode( $func_args ), DAY_IN_SECONDS ); 127 | 128 | /* Don't execute PHP internal error handler */ 129 | return $this->return_error_handler_result( true, $func_args ); 130 | } 131 | 132 | /** 133 | * Call the chain of other registered error handlers before returning the result. 134 | * 135 | * @param bool $handled Flag to indicate has the error already been handled. 136 | * @param array $args The arguments passed by PHP to our own registered error handler. 137 | * 138 | * @return bool True if the error has been handled, false if PHP error handler should still run. 139 | */ 140 | protected function return_error_handler_result( bool $handled, array $args ): bool { 141 | 142 | if ( is_null( $this->previous_error_handler ) ) { 143 | return $handled; 144 | } 145 | 146 | // If null is returned from the previous handler, treat that as if the error has not been handled by them. 147 | $handled_in_chain = call_user_func_array( $this->previous_error_handler, $args ) ?? false; 148 | return $handled_in_chain || $handled; 149 | } 150 | 151 | /** 152 | * Logic to check is the error relevant to this plugin. 153 | * 154 | * Check: 155 | * * is the source file path in the plugin directory 156 | * * is the plugin string mentioned in the error message 157 | * * is any file in the backtrace part of this plugin 158 | * 159 | * @param int $errno The error code (the level of the error raised, as an integer). 160 | * @param string $errstr A string describing the error. 161 | * @param string $errfile The filename in which the error occurred. 162 | * @param int $errline The line number in which the error occurred. 163 | * 164 | * @return bool 165 | */ 166 | protected function is_related_error( int $errno, string $errstr, string $errfile, int $errline ): bool { 167 | 168 | // If the source file has the plugin dir in it. 169 | // Prepend the WP_PLUGINS_DIR so a subdir with the same name (e.g. my-plugin/integrations/your-plugin) does not match. 170 | $plugin_dir = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $this->settings->get_plugin_slug(); 171 | $plugin_dir_realpath = realpath( $plugin_dir ); 172 | 173 | if ( false !== strpos( $errfile, $plugin_dir ) || ( false !== $plugin_dir_realpath && false !== strpos( $errfile, $plugin_dir_realpath ) ) ) { 174 | return true; 175 | } 176 | 177 | if ( false !== strpos( $errstr, $this->settings->get_plugin_slug() ) ) { 178 | // If the plugin slug is outright named in the error message. 179 | return true; 180 | } 181 | 182 | // e.g. WooCommerce Admin could be the $errfile of a problem caused by another plugin, so we need to... trace back. 183 | return $this->api->is_backtrace_contains_plugin( implode( '', func_get_args() ) ); 184 | } 185 | 186 | /** 187 | * Maps PHP's error types to PSR-3's error levels. 188 | * 189 | * Some of these will never occur at runtime. 190 | * 191 | * @see trigger_error() 192 | * @see https://www.php.net/manual/en/errorfunc.constants.php 193 | * 194 | * @param int $errno The PHP error type. 195 | * 196 | * @return string 197 | */ 198 | protected function errno_to_psr3( int $errno ): string { 199 | 200 | $error_types = array( 201 | E_ERROR => LogLevel::ERROR, 202 | E_CORE_ERROR => LogLevel::ERROR, 203 | E_COMPILE_ERROR => LogLevel::ERROR, 204 | E_USER_ERROR => LogLevel::ERROR, // User-generated error message – trigger_error(). 205 | E_RECOVERABLE_ERROR => LogLevel::ERROR, 206 | E_WARNING => LogLevel::WARNING, 207 | E_CORE_WARNING => LogLevel::WARNING, // Warnings (non-fatal errors) that occur during PHP's initial startup. 208 | E_COMPILE_WARNING => LogLevel::WARNING, // Compile-time warnings. 209 | E_USER_WARNING => LogLevel::WARNING, // User-generated warning message – trigger_error(). 210 | E_NOTICE => LogLevel::NOTICE, 211 | E_USER_NOTICE => LogLevel::NOTICE, 212 | E_DEPRECATED => LogLevel::NOTICE, 213 | E_USER_DEPRECATED => LogLevel::DEBUG, // User-generated warning message – trigger_error(). 214 | E_PARSE => LogLevel::ERROR, // Compile-time parse errors. 215 | ); 216 | 217 | if ( array_key_exists( $errno, $error_types ) ) { 218 | return $error_types[ $errno ]; 219 | } else { 220 | return LogLevel::ERROR; 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/php/class-php-shutdown-handler.php: -------------------------------------------------------------------------------- 1 | setLogger( $logger ); 47 | $this->settings = $settings; 48 | $this->api = $api; 49 | } 50 | 51 | /** 52 | * This should maybe be run immediately rather than hooked. It _is_ hooked to enable it to be unhooked. 53 | * 54 | * @hooked plugins_loaded 55 | */ 56 | public function init(): void { 57 | register_shutdown_function( array( $this, 'handle' ) ); 58 | } 59 | 60 | /** 61 | * The handler itself. Check is the error related to this plugin, then logs an error to the PSR logger. 62 | */ 63 | public function handle(): void { 64 | 65 | /** 66 | * The error from PHP. 67 | * 68 | * @var ?array{type:int, message:string, file:string, line:int} $error 69 | */ 70 | $error = error_get_last(); 71 | 72 | if ( empty( $error ) ) { 73 | return; 74 | } 75 | 76 | if ( ! $this->api->is_file_from_plugin( $error['file'] ) ) { 77 | return; 78 | } 79 | 80 | // "Clears the most recent errors, making it unable to be retrieved with error_get_last().". 81 | error_clear_last(); 82 | 83 | $this->logger->error( $error['message'], $error ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/private-uploads/class-url-is-public.php: -------------------------------------------------------------------------------- 1 | settings->get_plugin_slug()}._logger 27 | * 28 | * @param string $message The default message. 29 | * @param string $url The publicly accessible URL. 30 | * 31 | * @return string 32 | */ 33 | public function change_warning_message( string $message, string $url ): string { 34 | 35 | /* translators: %s: The URL where the log files are accessible. */ 36 | $new_message = sprintf( __( 'The logs directory is, and should not be, publicly accessible at the URL: %s. Please update your webserver configuration to block access to that folder.', 'bh-wp-logger' ), $url ); 37 | 38 | return $new_message; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/trait-logger-settings-trait.php: -------------------------------------------------------------------------------- 1 | get_plugin_slug() . '_log_level', LogLevel::INFO ); 42 | } catch ( \Exception $exception ) { 43 | return 'none'; 44 | } 45 | } 46 | 47 | /** 48 | * The plugin friendly name to use in UIs. 49 | * 50 | * @throws Exception When the basename cannot be determined. 51 | */ 52 | public function get_plugin_name(): string { 53 | // should this be get_plugin_data() ? 54 | return get_plugins()[$this->get_plugin_basename()]['Name']; 55 | } 56 | 57 | /** 58 | * The plugin slug. I.e. the plugin directory name. Used in URLs and wp_options. 59 | * 60 | * @throws Exception When the basename cannot be determined. 61 | */ 62 | public function get_plugin_slug(): string { 63 | return explode( '/', $this->get_plugin_basename() )[0]; 64 | } 65 | 66 | /** 67 | * The plugin basename. Used to add the Logs link on `plugins.php`. 68 | * 69 | * @see https://core.trac.wordpress.org/ticket/42670 70 | * 71 | * @throws Exception When it cannot be determined. I.e. a symlink inside a symlink. 72 | */ 73 | public function get_plugin_basename(): string { 74 | 75 | // TODO: The following might work but there are known issues around symlinks that need to be tested and handled correctly. 76 | // @see https://core.trac.wordpress.org/ticket/42670 77 | 78 | $wp_plugin_basename = plugin_basename( __DIR__ ); 79 | 80 | $plugin_filename = get_plugins( explode( '/', $wp_plugin_basename )[0] ); 81 | 82 | return array_key_first( $plugin_filename ); 83 | 84 | throw new Exception( 'Plugin installed in an unusual directory.' ); 85 | } 86 | 87 | /** 88 | * Default CLI commands to use the plugin slug as the base for commands. 89 | * 90 | * @see CLI 91 | */ 92 | public function get_cli_base(): ?string { 93 | return $this->get_plugin_slug(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/wp-includes/class-cli.php: -------------------------------------------------------------------------------- 1 | setLogger( $logger ); 41 | $this->settings = $settings; 42 | $this->api = $api; 43 | } 44 | 45 | /** 46 | * Register the WP-CLI commands. 47 | * 48 | * Use `null` to disable CLI commands. 49 | * The settings trait uses the plugin slug as the default CLI base. 50 | * 51 | * @see Logger_Settings_Trait::get_cli_base() 52 | */ 53 | public function register_commands(): void { 54 | 55 | $cli_base = $this->settings->get_cli_base(); 56 | 57 | if ( is_null( $cli_base ) ) { 58 | return; 59 | } 60 | 61 | try { 62 | WP_CLI::add_command( "{$cli_base} logger delete-all", array( $this, 'delete_all_logs' ) ); 63 | } catch ( Exception $e ) { 64 | $this->logger->error( 65 | 'Failed to register WP CLI commands: ' . $e->getMessage(), 66 | array( 'exception' => $e ) 67 | ); 68 | } 69 | } 70 | 71 | /** 72 | * Delete all logs. 73 | * 74 | * ## EXAMPLES 75 | * 76 | * # Delete all logs for the plugin. 77 | * $ wp $cli_base logger delete-all 78 | * Success: Deleted 12 log files. 79 | * 80 | * @param string[] $args The unlabelled command line arguments. 81 | * @param array $assoc_args The labelled command line arguments. 82 | * 83 | * @see API_Interface::get_licence_details() 84 | */ 85 | public function delete_all_logs( array $args, array $assoc_args ): void { 86 | 87 | try { 88 | $result = $this->api->delete_all_logs(); 89 | } catch ( Exception $e ) { 90 | WP_CLI::error( $e->getMessage() ); 91 | } 92 | 93 | WP_CLI::success('Deleted ' . count($result['deleted_files']) . ' log files.' ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/wp-includes/class-cron.php: -------------------------------------------------------------------------------- 1 | setLogger( $logger ); 49 | $this->settings = $settings; 50 | $this->api = $api; 51 | } 52 | 53 | /** 54 | * Schedule a daily cron job to delete old logs, just after midnight. 55 | * 56 | * Does not schedule the cleanup if it is a WooCommerce logger (since WooCommerce handles that itself). 57 | * 58 | * @hooked init 59 | */ 60 | public function register_delete_logs_cron_job(): void { 61 | 62 | /** 63 | * Cast the logger to the logger facade so we can access the true logger itself. 64 | * 65 | * @var BH_WP_PSR_Logger $bh_wp_psr_logger 66 | */ 67 | $bh_wp_psr_logger = $this->logger; 68 | $logger = $bh_wp_psr_logger->get_logger(); 69 | 70 | if ( $logger instanceof WC_PSR_Logger ) { 71 | return; 72 | } 73 | 74 | $cron_hook = "delete_logs_{$this->settings->get_plugin_slug()}"; 75 | 76 | if ( false !== wp_get_scheduled_event( $cron_hook ) ) { 77 | return; 78 | } 79 | 80 | wp_schedule_event( strtotime( 'tomorrow' ), 'daily', $cron_hook ); 81 | 82 | $this->logger->debug( "Registered the `{$cron_hook}` cron job." ); 83 | } 84 | 85 | /** 86 | * Handle the cron job. 87 | * 88 | * @hooked delete_logs_{plugin-slug} 89 | */ 90 | public function delete_old_logs(): void { 91 | $action = current_action(); 92 | $this->logger->debug( "Executing {$action} cron job." ); 93 | 94 | $this->api->delete_old_logs(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/wp-includes/class-init.php: -------------------------------------------------------------------------------- 1 | setLogger( $logger ); 46 | $this->settings = $settings; 47 | $this->api = $api; 48 | } 49 | 50 | /** 51 | * Check is this a request to download a log. 52 | * Return quietly if not. 53 | * Fail hard if nonce is incorrect. 54 | * Return if required parameters missing. 55 | * Return if plugin slug does not match or if date is malformed. 56 | * Invoke `send_private_file()` to download the file. 57 | * 58 | * This is really only needed when WooCommerce logger is being used because it store the log files in 59 | * `/uploads/wc-logs` which has a `.htaccess` preventing downloads. 60 | * 61 | * @hooked init 62 | */ 63 | public function maybe_download_log(): void { 64 | 65 | if ( ! isset( $_GET['download-log'] ) ) { 66 | return; 67 | } 68 | 69 | if ( false === check_admin_referer( 'bh-wp-logger-download' ) ) { 70 | $this->logger->warning( 'Bad nonce when downloading log.' ); 71 | wp_die(); 72 | return; // Needed for tests. @phpstan-ignore-line. 73 | } 74 | 75 | if ( 'true' !== sanitize_text_field( wp_unslash( $_GET['download-log'] ) ) ) { 76 | return; 77 | } 78 | 79 | if ( ! isset( $_GET['page'] ) || ! isset( $_GET['date'] ) ) { 80 | return; 81 | } 82 | 83 | $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); 84 | 85 | if ( 0 !== strpos( $page, $this->settings->get_plugin_slug() ) ) { 86 | return; 87 | } 88 | 89 | $date = sanitize_text_field( wp_unslash( $_GET['date'] ) ); 90 | 91 | if ( 1 !== preg_match( '/\d{4}-\d{2}-\d{2}/', $date ) ) { 92 | return; 93 | } 94 | 95 | $files = $this->api->get_log_files( $date ); 96 | 97 | if ( ! isset( $files[ $date ] ) ) { 98 | return; 99 | } 100 | 101 | $file = $files[ $date ]; 102 | 103 | $this->send_private_file( $file ); 104 | } 105 | 106 | /** 107 | * Set the correct headers, send the file, die. 108 | * 109 | * @param string $filepath The requested filename. 110 | * 111 | * @see Serve_Private_File::send_private_file() 112 | * 113 | * Nonce was checked above. 114 | * 115 | * phpcs:disable WordPress.Security.NonceVerification.Recommended 116 | */ 117 | protected function send_private_file( string $filepath ): void { 118 | 119 | // Add the mimetype header. 120 | $mime = wp_check_filetype( $filepath ); // This function just looks at the extension. 121 | $mimetype = $mime['type']; 122 | if ( ! $mimetype && function_exists( 'mime_content_type' ) ) { 123 | 124 | $mimetype = mime_content_type( $filepath ); // Use ext-fileinfo to look inside the file. 125 | } 126 | if ( ! $mimetype ) { 127 | $mimetype = 'application/octet-stream'; 128 | } 129 | 130 | header( 'Content-type: ' . $mimetype ); // always send this. 131 | header( 'Content-Disposition: attachment; filename="' . basename( $filepath ) . '"' ); 132 | 133 | // Add timing headers. 134 | $date_format = 'D, d M Y H:i:s T'; // RFC2616 date format for HTTP. 135 | $last_modified_unix = (int) filemtime( $filepath ); 136 | $last_modified = gmdate( $date_format, $last_modified_unix ); 137 | $etag = md5( $last_modified ); 138 | header( "Last-Modified: $last_modified" ); 139 | header( 'ETag: "' . $etag . '"' ); 140 | header( 'Expires: ' . gmdate( $date_format, time() + HOUR_IN_SECONDS ) ); // an arbitrary hour from now. 141 | 142 | // Support for caching. 143 | $client_etag = isset( $_REQUEST['HTTP_IF_NONE_MATCH'] ) ? trim( sanitize_text_field( wp_unslash( $_REQUEST['HTTP_IF_NONE_MATCH'] ) ) ) : ''; 144 | $client_if_mod_since = isset( $_REQUEST['HTTP_IF_MODIFIED_SINCE'] ) ? trim( sanitize_text_field( wp_unslash( $_REQUEST['HTTP_IF_MODIFIED_SINCE'] ) ) ) : ''; 145 | $client_if_mod_since_unix = strtotime( $client_if_mod_since ); 146 | 147 | if ( $etag === $client_etag || $last_modified_unix <= $client_if_mod_since_unix ) { 148 | // Return 'not modified' header. 149 | status_header( 304 ); 150 | die(); 151 | } 152 | 153 | // If we made it this far, just serve the file. 154 | status_header( 200 ); 155 | // (WP_Filesystem is only loaded for admin requests, not applicable here). 156 | // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_readfile 157 | readfile( $filepath ); 158 | die(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/wp-includes/class-plugin-logger-actions.php: -------------------------------------------------------------------------------- 1 | wrapped_real_logger = $wrapped_real_logger; 60 | $this->settings = $settings; 61 | $this->api = $api; 62 | 63 | $this->add_error_handler_hooks(); 64 | 65 | $this->add_admin_ui_logs_page_hooks(); 66 | $this->add_admin_notices_hooks(); 67 | $this->add_ajax_hooks(); 68 | $this->add_plugins_page_hooks(); 69 | $this->add_plugin_installer_page_hooks(); 70 | $this->add_cron_hooks(); 71 | $this->add_private_uploads_hooks(); 72 | $this->define_init_hooks(); 73 | $this->define_cli_hooks(); 74 | 75 | if ( ! $this->is_wp_debug() ) { 76 | return; 77 | } 78 | 79 | $this->add_wordpress_error_handling_hooks(); 80 | } 81 | 82 | /** 83 | * 84 | * 85 | * Defined as a protected function so it can be overridden. 86 | * Caution: only consider overriding this when running on your own site. This 87 | * adds significant inefficiency. 88 | */ 89 | protected function is_wp_debug(): bool { 90 | return defined( 'WP_DEBUG' ) && WP_DEBUG; 91 | } 92 | 93 | /** 94 | * Add error handling for PHP errors and shutdowns. 95 | */ 96 | protected function add_error_handler_hooks(): void { 97 | 98 | $php_error_handler = new PHP_Error_Handler( $this->api, $this->settings, $this->wrapped_real_logger ); 99 | add_action( 'plugins_loaded', array( $php_error_handler, 'init' ), 2 ); 100 | 101 | $php_shutdown_handler = new PHP_Shutdown_Handler( $this->api, $this->settings, $this->wrapped_real_logger ); 102 | add_action( 'plugins_loaded', array( $php_shutdown_handler, 'init' ), 2 ); 103 | } 104 | 105 | /** 106 | * Add hooks to WordPress's handling of deprecated functions etc. in order to log it ourselves. 107 | * 108 | * Only runs in WP_DEBUG because this runs a backtrace on every entry to the error log, which can be significant 109 | * when other plugins are not "clean". 110 | */ 111 | protected function add_wordpress_error_handling_hooks(): void { 112 | 113 | if ( ! $this->is_wp_debug() ) { 114 | return; 115 | } 116 | 117 | $functions = new Functions( $this->api, $this->settings, $this->wrapped_real_logger ); 118 | 119 | add_action( 'deprecated_function_run', array( $functions, 'log_deprecated_functions_only_once_per_day' ), 10, 3 ); 120 | add_action( 'deprecated_argument_run', array( $functions, 'log_deprecated_arguments_only_once_per_day' ), 10, 3 ); 121 | add_action( 'doing_it_wrong_run', array( $functions, 'log_doing_it_wrong_only_once_per_day' ), 10, 3 ); 122 | add_action( 'deprecated_hook_run', array( $functions, 'log_deprecated_hook_only_once_per_day' ), 10, 4 ); 123 | } 124 | 125 | /** 126 | * Register dismissable admin notices for recorded logs. 127 | */ 128 | protected function add_admin_notices_hooks(): void { 129 | 130 | $admin_notices = new Admin_Notices( $this->api, $this->settings ); 131 | // Generate the notices from wp_options. 132 | add_action( 'admin_init', array( $admin_notices, 'admin_notices' ), 9 ); 133 | // Add the notice. 134 | add_action( 'admin_notices', array( $admin_notices, 'the_notices' ) ); 135 | } 136 | 137 | /** 138 | * Add an admin UI page to display the logs table. 139 | * Enqueue the JavaScript for handling the buttons. 140 | */ 141 | protected function add_admin_ui_logs_page_hooks(): void { 142 | 143 | $logs_page = new Logs_Page( $this->api, $this->settings, $this->wrapped_real_logger ); 144 | add_action( 'admin_menu', array( $logs_page, 'add_page' ), 20 ); 145 | add_action( 'admin_enqueue_scripts', array( $logs_page, 'enqueue_scripts' ) ); 146 | add_action( 'admin_enqueue_scripts', array( $logs_page, 'enqueue_styles' ) ); 147 | } 148 | 149 | /** 150 | * Enqueue AJAX handlers for the logs page's buttons. 151 | */ 152 | protected function add_ajax_hooks(): void { 153 | 154 | $ajax = new AJAX( $this->api, $this->settings ); 155 | 156 | add_action( 'wp_ajax_bh_wp_logger_logs_delete', array( $ajax, 'delete' ) ); 157 | add_action( 'wp_ajax_bh_wp_logger_logs_delete_all', array( $ajax, 'delete_all' ) ); 158 | } 159 | 160 | /** 161 | * Add link on plugins.php to the logs page. 162 | */ 163 | protected function add_plugins_page_hooks(): void { 164 | 165 | $plugins_page = new Plugins_Page( $this->api, $this->settings ); 166 | 167 | $hook = "plugin_action_links_{$this->settings->get_plugin_basename()}"; 168 | add_filter( $hook, array( $plugins_page, 'add_logs_action_link' ), 99, 4 ); 169 | } 170 | 171 | /** 172 | * Add link to Logs on the plugin installer page (after installing a plugin via .zip file). 173 | * 174 | * Hooked late because the logs link should be last. 175 | */ 176 | protected function add_plugin_installer_page_hooks(): void { 177 | 178 | $plugin_installer = new Plugin_Installer( $this->settings ); 179 | 180 | add_filter( 'install_plugin_complete_actions', array( $plugin_installer, 'add_logs_link' ), 99, 3 ); 181 | } 182 | 183 | /** 184 | * Schedule a job to clean up logs. 185 | */ 186 | protected function add_cron_hooks(): void { 187 | 188 | $cron = new Cron( $this->api, $this->settings, $this->wrapped_real_logger ); 189 | 190 | add_action( 'init', array( $cron, 'register_delete_logs_cron_job' ) ); 191 | add_action( 'delete_logs_' . $this->settings->get_plugin_slug(), array( $cron, 'delete_old_logs' ) ); 192 | } 193 | 194 | /** 195 | * Add filter to change the admin notice when the logs directory is publicly accessible. 196 | * 197 | * @see \BrianHenryIE\WP_Private_Uploads\Admin\Admin_Notices::admin_notices() 198 | */ 199 | protected function add_private_uploads_hooks(): void { 200 | 201 | $url_is_public = new URL_Is_Public(); 202 | 203 | add_filter( "bh_wp_private_uploads_url_is_public_warning_{$this->settings->get_plugin_slug()}_logger", array( $url_is_public, 'change_warning_message' ), 10, 2 ); 204 | } 205 | 206 | /** 207 | * Hook in to init to download log files. 208 | */ 209 | protected function define_init_hooks(): void { 210 | 211 | $init = new Init( $this->api, $this->settings, $this->wrapped_real_logger ); 212 | 213 | add_action( 'init', array( $init, 'maybe_download_log' ) ); 214 | } 215 | 216 | /** 217 | * Add CLI commands to delete logs. 218 | * 219 | * Use `null` to disable CLI commands. 220 | * The settings trait uses the plugin slug as the default CLI base. 221 | * 222 | * @see Logger_Settings_Trait::get_cli_base() 223 | */ 224 | protected function define_cli_hooks(): void { 225 | 226 | $cli_base = $this->settings->get_cli_base(); 227 | 228 | if ( is_null( $cli_base ) ) { 229 | return; 230 | } 231 | 232 | $cli = new CLI( $this->api, $this->settings, $this->wrapped_real_logger ); 233 | 234 | add_action( 'cli_init', array( $cli, 'register_commands' ) ); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /test-plugin/Admin/class-admin-ajax.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 28 | } 29 | 30 | /** 31 | * @hooked wp_ajax_log 32 | */ 33 | public function handle_request() { 34 | 35 | $result = array(); 36 | $result['error'] = array(); 37 | $result['success'] = array(); 38 | 39 | // if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'logs-test' ) ) { 40 | // 41 | // $result['error']['nonce-failed'] ='Nonce verification failed. Try reloading the page.'; 42 | // 43 | // TODO: Should not return HTTP status 200? ... 403. 44 | // wp_send_json( $result, 403 ); 45 | // 46 | // } 47 | 48 | // Validate input. 49 | 50 | if ( ! isset( $_POST['log-test-action'] ) || empty( $_POST['log-test-action'] ) ) { 51 | $result['error']['missing-log-test-action'] = 'Missing log-test-action parameter.'; 52 | 53 | } else { 54 | $log_test_action = wp_unslash( $_POST['log-test-action'] ); 55 | 56 | $message = isset( $_POST['message'] ) ? esc_html( wp_unslash( $_POST['message'] ) ) : null; 57 | $context = isset( $_POST['context'] ) ? explode( ',', esc_html( wp_unslash( $_POST['context'] ) ) ) : array(); 58 | 59 | switch ( $log_test_action ) { 60 | case 'debug-message': 61 | $this->logger->debug( $message ?? 'log test debug message', $context ); 62 | break; 63 | case 'info-message': 64 | $this->logger->info( $message ?? 'log test info message', $context ); 65 | break; 66 | case 'notice-message': 67 | $this->logger->notice( $message ?? 'log test notice message', $context ); 68 | break; 69 | case 'warning-message': 70 | $this->logger->warning( $message ?? 'log test warning message', $context ); 71 | break; 72 | case 'error-message': 73 | $this->logger->error( $message ?? 'log test error message', $context ); 74 | break; 75 | case 'deprecated-php': 76 | trigger_error( 'log test deprecated php', E_USER_DEPRECATED ); 77 | break; 78 | case 'notice-php': 79 | trigger_error( 'log test notice php', E_USER_NOTICE ); 80 | break; 81 | case 'warning-php': 82 | trigger_error( 'log test warning php', E_USER_WARNING ); 83 | break; 84 | case 'error-php': 85 | trigger_error( 'log test error php', E_USER_ERROR ); 86 | break; 87 | case 'doing_it_wrong_run-wordpress': 88 | _doing_it_wrong( 'is_allowed_dir', 'The "$dir" argument must be a non-empty string.', '6.2.0' ); 89 | break; 90 | case 'deprecated_function_run-wordpress': 91 | _deprecated_function( 'tinymce_include', '2.1.0', 'wp_editor()' ); 92 | break; 93 | case 'deprecated_argument_run-wordpress': 94 | _deprecated_argument( 'wp_editor()', '3.9.0', 'TinyMCE editor IDs cannot have brackets.' ); 95 | break; 96 | case 'deprecated_hook_run-wordpress': 97 | _deprecated_hook( 'hook_name', 'version', 'replacement', 'message' ); 98 | break; 99 | case 'uncaught-exception': 100 | throw new \Exception( 'log test exception' ); 101 | case 'delete-transients': 102 | global $wpdb; 103 | $result = $wpdb->query( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE "_transient_%"' ); 104 | $result = array(); 105 | break; 106 | default: 107 | $result['error']['unknown-log-test-action'] = 'Unknown log-test-action parameter.'; 108 | break; 109 | } 110 | } 111 | 112 | if ( ! empty( $result['error'] ) ) { 113 | wp_send_json( $result, 400 ); 114 | } 115 | wp_send_json( $result ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test-plugin/Admin/class-admin.php: -------------------------------------------------------------------------------- 1 | logger_settings = $logger_settings; 33 | $this->logger = $logger; 34 | } 35 | 36 | /** 37 | * Register the stylesheets for the admin area. 38 | * 39 | * @since 1.0.0 40 | */ 41 | public function enqueue_styles() { 42 | 43 | $version = time(); 44 | 45 | $url = WP_PLUGIN_URL . '/bh-wp-logger-test-plugin/Admin/css/bh-wp-logger-test-plugin-admin.css'; 46 | 47 | wp_enqueue_style( 'bh-wp-logger-test-plugin', $url, array(), $version, 'all' ); 48 | } 49 | 50 | /** 51 | * Register the JavaScript for the admin area. 52 | * 53 | * @since 1.0.0 54 | */ 55 | public function enqueue_scripts() { 56 | 57 | $version = time(); 58 | 59 | $url = WP_PLUGIN_URL . '/bh-wp-logger-test-plugin/Admin/js/bh-wp-logger-test-plugin-admin.js'; 60 | 61 | wp_enqueue_script( 'bh-wp-logger-test-plugin', $url, array( 'jquery' ), $version, true ); 62 | } 63 | 64 | /** 65 | * Register the callback to the new page, adding the link in the admin menu. 66 | * 67 | * @hooked admin_menu 68 | */ 69 | public function add_page() { 70 | 71 | $icon_url = 'dashicons-text-page'; 72 | 73 | add_menu_page( 74 | 'Logs Test', 75 | 'Logs Test', 76 | 'manage_options', 77 | 'logs-test', 78 | array( $this, 'display_page' ), 79 | $icon_url, 80 | 2 81 | ); 82 | } 83 | 84 | /** 85 | * Registered in @see add_page() 86 | */ 87 | public function display_page() { 88 | 89 | $plugin_log_level = $this->logger_settings->get_log_level(); 90 | 91 | $is_woocommerce_logger = $this->logger_settings instanceof WooCommerce_Logger_Settings_Interface ? 'yes' : 'no'; 92 | 93 | /** @var API_Interface $plugin_logger_api */ 94 | $plugin_logger_api = $this->logger; 95 | 96 | $log_files = $plugin_logger_api->get_log_files(); 97 | $plugin_log_file = array_pop( $log_files ); 98 | 99 | $plugin_log_url = $plugin_logger_api->get_log_url(); 100 | 101 | $wp_debug = defined( 'WP_DEBUG' ) && WP_DEBUG ? 'enabled' : 'disabled'; 102 | $wp_debug_display = defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ? 'enabled' : 'disabled'; 103 | $wp_debug_log = 'disabled'; 104 | if ( defined( 'WP_DEBUG_LOG' ) ) { 105 | $wp_debug_log = true === WP_DEBUG_LOG ? 'enabled' : WP_DEBUG_LOG; 106 | } 107 | 108 | include wp_normalize_path( __DIR__ . '/partials/bh-wp-logger-test-plugin-admin-display.php' ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test-plugin/Admin/css/bh-wp-logger-test-plugin-admin.css: -------------------------------------------------------------------------------- 1 | /** 2 | * All of the CSS for your admin-specific functionality should be 3 | * included in this file. 4 | */ -------------------------------------------------------------------------------- /test-plugin/Admin/js/bh-wp-logger-test-plugin-admin.js: -------------------------------------------------------------------------------- 1 | 2 | (function( $ ) { 3 | 'use strict'; 4 | 5 | $(function() { 6 | 7 | $('.button.log-test').on('click', function(event) { 8 | event.preventDefault(); 9 | 10 | window.console.log("You clicked on: " + event.target.name); 11 | 12 | var data = { 13 | 'action': 'log', 14 | 'log-test-action': event.target.name 15 | }; 16 | 17 | let message = document.getElementById('log_message').value; 18 | if(message) { 19 | data.message = message; 20 | } 21 | let context = document.getElementById('log_context').value; 22 | if(context) { 23 | data.context = context; 24 | } 25 | 26 | 27 | $.post(ajaxurl, data, function (response) { 28 | 29 | 30 | // TODO: Show & refresh the log table below. 31 | 32 | 33 | }); 34 | 35 | }); 36 | 37 | }); 38 | 39 | })( jQuery ); 40 | -------------------------------------------------------------------------------- /test-plugin/Admin/partials/bh-wp-logger-test-plugin-admin-display.php: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 |
26 |

Logs Test

27 |

Some buttons for writing to the logs.

28 | 29 |
30 | 31 |

Plugin log level is:

32 |

Plugin is using wc_logger():

33 |

Plugin log file is at:

34 |

WP_DEBUG:

35 |

WP_DEBUG_DISPLAY:

36 |

WP_DEBUG_LOG:

37 |
38 |
39 |

View logs

40 |
41 |
42 |

Log a message

43 | 44 |
$logger->info('message', 'context');
45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 |
59 | 60 |
61 |

Trigger a PHP error

62 | 63 |
trigger_error( 'message', E_USER_NOTICE );
64 | 65 |
66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | 74 |
75 |

Trigger a WordPress doing_it_wrong warning

76 | 77 |
78 | 79 | 80 | 81 | 82 |
83 |
84 | 85 |
86 |

Throw an [uncaught] Exception

87 | 88 |
throw new \Exception( 'log test exception' );
89 | 90 |
91 | 92 |
93 |
94 | 95 | 96 |
97 |

Delete all transients

98 | 99 |

Transients are used to prevent duplicate logs, in some cases.

100 | 101 |
$wpdb->query('DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE "_transient_%"');
102 | 103 |
104 | 105 |
106 |
107 | 108 |
109 |
110 | -------------------------------------------------------------------------------- /test-plugin/Includes/class-bh-wp-logger-test-plugin.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | class BH_WP_Logger_Test_Plugin { 37 | 38 | /** 39 | * The logger we're testing! 40 | * 41 | * @var LoggerInterface 42 | */ 43 | protected BH_Logger $logger; 44 | 45 | protected $settings; 46 | 47 | /** 48 | * Define the core functionality of the plugin. 49 | * 50 | * Set the plugin name and the plugin version that can be used throughout the plugin. 51 | * Load the dependencies, define the locale, and set the hooks for the admin area and 52 | * the frontend-facing side of the site. 53 | * 54 | * @since 1.0.0 55 | * 56 | * @param LoggerInterface $logger A PSR Logger. 57 | */ 58 | public function __construct( $settings, BH_Logger $logger ) { 59 | 60 | $this->settings = $settings; 61 | $this->logger = $logger; 62 | 63 | $this->set_locale(); 64 | $this->define_admin_hooks(); 65 | } 66 | 67 | /** 68 | * Define the locale for this plugin for internationalization. 69 | * 70 | * Uses the i18n class in order to set the domain and to register the hook 71 | * with WordPress. 72 | * 73 | * @since 1.0.0 74 | * @access private 75 | */ 76 | protected function set_locale() { 77 | 78 | $plugin_i18n = new I18n(); 79 | 80 | add_action( 'plugins_loaded', array( $plugin_i18n, 'load_plugin_textdomain' ) ); 81 | } 82 | 83 | /** 84 | * Register all of the hooks related to the admin area functionality 85 | * of the plugin. 86 | * 87 | * @since 1.0.0 88 | * @access private 89 | */ 90 | protected function define_admin_hooks() { 91 | 92 | $plugin_admin = new Admin( $this->settings, $this->logger ); 93 | 94 | add_action( 'admin_enqueue_scripts', array( $plugin_admin, 'enqueue_styles' ) ); 95 | add_action( 'admin_enqueue_scripts', array( $plugin_admin, 'enqueue_scripts' ) ); 96 | 97 | add_action( 'admin_menu', array( $plugin_admin, 'add_page' ) ); 98 | 99 | // Handle actions on the admin page. 100 | $admin_ajax = new Admin_Ajax( $this->logger ); 101 | add_action( 'wp_ajax_log', array( $admin_ajax, 'handle_request' ) ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test-plugin/Includes/class-i18n.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class I18n { 29 | 30 | 31 | /** 32 | * Load the plugin text domain for translation. 33 | * 34 | * @since 1.0.0 35 | */ 36 | public function load_plugin_textdomain() { 37 | 38 | load_plugin_textdomain( 39 | 'bh-wp-logger-test-plugin', 40 | false, 41 | dirname( dirname( plugin_basename( __FILE__ ) ) ) . '/languages/' 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test-plugin/README.txt: -------------------------------------------------------------------------------- 1 | === BH WP Logger Test Plugin === 2 | Contributors: (this should be a list of wordpress.org userid's) 3 | Donate link: http://example.com/ 4 | Tags: comments, spam 5 | Requires at least: 3.0.1 6 | Tested up to: 3.4 7 | Stable tag: 4.3 8 | License: GPLv2 or later 9 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | Here is a short description of the plugin. This should be no more than 150 characters. No markup here. 12 | 13 | == Description == 14 | 15 | This is the long description. No limit, and you can use Markdown (as well as in the following sections). 16 | 17 | For backwards compatibility, if this section is missing, the full length of the short description will be used, and 18 | Markdown parsed. 19 | 20 | A few notes about the sections above: 21 | 22 | * "Contributors" is a comma separated list of wp.org/wp-plugins.org usernames 23 | * "Tags" is a comma separated list of tags that apply to the plugin 24 | * "Requires at least" is the lowest version that the plugin will work on 25 | * "Tested up to" is the highest version that you've *successfully used to test the plugin*. Note that it might work on 26 | higher versions... this is just the highest one you've verified. 27 | * Stable tag should indicate the Subversion "tag" of the latest stable version, or "trunk," if you use `/trunk/` for 28 | stable. 29 | 30 | Note that the `readme.txt` of the stable tag is the one that is considered the defining one for the plugin, so 31 | if the `/trunk/readme.txt` file says that the stable tag is `4.3`, then it is `/tags/4.3/readme.txt` that'll be used 32 | for displaying information about the plugin. In this situation, the only thing considered from the trunk `readme.txt` 33 | is the stable tag pointer. Thus, if you develop in trunk, you can update the trunk `readme.txt` to reflect changes in 34 | your in-development version, without having that information incorrectly disclosed about the current stable version 35 | that lacks those changes -- as long as the trunk's `readme.txt` points to the correct stable tag. 36 | 37 | If no stable tag is provided, it is assumed that trunk is stable, but you should specify "trunk" if that's where 38 | you put the stable version, in order to eliminate any doubt. 39 | 40 | == Installation == 41 | 42 | This section describes how to install the plugin and get it working. 43 | 44 | e.g. 45 | 46 | 1. Upload `bh-wp-logger-test-plugin.php` to the `/wp-content/plugins/` directory 47 | 1. Activate the plugin through the 'Plugins' menu in WordPress 48 | 1. Place `` in your templates 49 | 50 | == Frequently Asked Questions == 51 | 52 | = A question that someone might have = 53 | 54 | An answer to that question. 55 | 56 | = What about foo bar? = 57 | 58 | Answer to foo bar dilemma. 59 | 60 | == Screenshots == 61 | 62 | 1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from 63 | the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets 64 | directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png` 65 | (or jpg, jpeg, gif). 66 | 2. This is the second screen shot 67 | 68 | == Changelog == 69 | 70 | = 1.0 = 71 | * A change since the previous version. 72 | * Another change. 73 | 74 | = 0.5 = 75 | * List versions from most recent at top to oldest at bottom. 76 | 77 | == Upgrade Notice == 78 | 79 | = 1.0 = 80 | Upgrade notices describe the reason a user should upgrade. No more than 300 characters. 81 | 82 | = 0.5 = 83 | This version fixes a security related bug. Upgrade immediately. 84 | 85 | == Arbitrary section == 86 | 87 | You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated 88 | plugins where more information needs to be conveyed that doesn't fit into the categories of "description" or 89 | "installation." Arbitrary sections will be shown below the built-in sections outlined above. 90 | 91 | == A brief Markdown Example == 92 | 93 | Ordered list: 94 | 95 | 1. Some feature 96 | 1. Another feature 97 | 1. Something else about the plugin 98 | 99 | Unordered list: 100 | 101 | * something 102 | * something else 103 | * third thing 104 | 105 | Here's a link to [WordPress](http://wordpress.org/ "Your favorite software") and one to [Markdown's Syntax Documentation][markdown syntax]. 106 | Titles are optional, naturally. 107 | 108 | [markdown syntax]: http://daringfireball.net/projects/markdown/syntax 109 | "Markdown is what the parser uses to process much of the readme file" 110 | 111 | Markdown uses email style notation for blockquotes and I've been told: 112 | > Asterisks for *emphasis*. Double it up for **strong**. 113 | 114 | `` 115 | -------------------------------------------------------------------------------- /test-plugin/bh-wp-logger-test-plugin.php: -------------------------------------------------------------------------------- 1 | register(); 48 | 49 | /** 50 | * Current plugin version. 51 | * Start at version 1.0.0 and use SemVer - https://semver.org 52 | * Rename this for your plugin and update it as you release new versions. 53 | */ 54 | define( 'BH_WP_LOGGER_TEST_PLUGIN_VERSION', '1.0.0' ); 55 | 56 | /** 57 | * Begins execution of the plugin. 58 | * 59 | * Since everything within the plugin is registered via hooks, 60 | * then kicking off the plugin from this point in the file does 61 | * not affect the page life cycle. 62 | * 63 | * @since 1.0.0 64 | */ 65 | function instantiate_bh_wp_logger_test_plugin() { 66 | 67 | $logger_settings = new class( 'bh-wp-logger-test-plugin' ) implements Logger_Settings_Interface { //}, WooCommerce_Logger_Settings_Interface { 68 | use Logger_Settings_Trait; 69 | 70 | public function get_log_level(): string { 71 | return LogLevel::DEBUG; 72 | } 73 | public function get_plugin_slug(): string { 74 | return 'bh-wp-logger-test-plugin'; 75 | } 76 | public function get_plugin_basename(): string { 77 | return 'bh-wp-logger-test-plugin/bh-wp-logger-test-plugin.php'; 78 | } 79 | public function get_plugin_name(): string { 80 | return 'BH WP Logger Test Plugin'; 81 | } 82 | }; 83 | 84 | $logger = Logger::instance( $logger_settings ); 85 | 86 | $plugin = new BH_WP_Logger_Test_Plugin( $logger_settings, $logger ); 87 | 88 | return $plugin; 89 | } 90 | $GLOBALS['bh_wp_logger_test_plugin'] = instantiate_bh_wp_logger_test_plugin(); 91 | 92 | /** 93 | * Pass in a closure to be executed, so the backtrace will contain the plugin. 94 | * For integration tests. 95 | * 96 | * @param $closure 97 | * 98 | * @return void 99 | */ 100 | function run_closure_in_plugin( $closure ) { 101 | $closure(); 102 | } 103 | 104 | add_filter( 105 | 'plugins_url', 106 | function ( $url ) { 107 | return str_replace( 'Users/brianhenry/Sites', 'bh-wp-logger-test-plugin/vendor/brianhenryie', $url ); 108 | } 109 | ); 110 | -------------------------------------------------------------------------------- /test-plugin/languages/bh-wp-logger-test-plugin.pot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianHenryIE/bh-wp-logger/0a4b6247f42b3717147d9f5db55b7e62799514af/test-plugin/languages/bh-wp-logger-test-plugin.pot -------------------------------------------------------------------------------- /test-plugin/uninstall.php: -------------------------------------------------------------------------------- 1 | error call. 15 | */ 16 | public function test_backtrace_excludes_logger_files(): void { 17 | 18 | // Integration test has already loaded the test plugin. 19 | $logger = Logger::instance(); 20 | 21 | $logger->error( __FUNCTION__ ); 22 | 23 | $log_file_paths = $logger->get_log_files(); 24 | 25 | $log_file_path = array_pop( $log_file_paths ); 26 | 27 | $log_contents = file_get_contents( $log_file_path ); 28 | $log_lines = explode( "\n", $log_contents ); 29 | 30 | array_pop( $log_lines ); // blank line at end of file. 31 | 32 | $context_line = array_pop( $log_lines ); 33 | $context = json_decode( $context_line, true ); 34 | 35 | $debug_backtrace = $context['debug_backtrace']; 36 | 37 | $this->assertEquals( __FILE__, $debug_backtrace[0]['file'] ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/integration/class-bh-wp-logger-develop-Test.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace BH_WP_Logger_Test_Plugin; 10 | 11 | use BH_WP_Logger_Test_Plugin\WP_Includes\BH_WP_Logger_Test_Plugin; 12 | 13 | /** 14 | * Verifies the plugin has been instantiated and added to PHP's $GLOBALS variable. 15 | * 16 | * @coversNothing 17 | */ 18 | class Plugin_Develop_Test extends \Codeception\TestCase\WPTestCase { 19 | 20 | /** 21 | * Test the main plugin object is added to PHP's GLOBALS and that it is the correct class. 22 | */ 23 | public function test_plugin_instantiated() { 24 | 25 | $this->assertArrayHasKey( 'bh_wp_logger_test_plugin', $GLOBALS ); 26 | 27 | $this->assertInstanceOf( BH_WP_Logger_Test_Plugin::class, $GLOBALS['bh_wp_logger_test_plugin'] ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/integration/wp-includes/class-bh-wp-logger-integration-Test.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace BH_WP_Logger_Test_Plugin\WP_Includes; 10 | 11 | use BH_WP_Logger_Test_Plugin\Admin\Admin; 12 | 13 | /** 14 | * @coversNothing 15 | */ 16 | class BH_WP_Logger_Integration_Test extends \Codeception\TestCase\WPTestCase { 17 | 18 | /** 19 | * Verify admin_enqueue_scripts action is correctly added for styles, at priority 10. 20 | */ 21 | public function test_action_admin_enqueue_scripts_styles() { 22 | 23 | $action_name = 'admin_enqueue_scripts'; 24 | $expected_priority = 10; 25 | $class_type = Admin::class; 26 | $method_name = 'enqueue_styles'; 27 | 28 | global $wp_filter; 29 | 30 | $this->assertArrayHasKey( $action_name, $wp_filter, "$method_name definitely not hooked to $action_name" ); 31 | 32 | $actions_hooked = $wp_filter[ $action_name ]; 33 | 34 | $this->assertArrayHasKey( $expected_priority, $actions_hooked, "$method_name definitely not hooked to $action_name priority $expected_priority" ); 35 | 36 | $hooked_methods = array(); 37 | foreach ( $actions_hooked[ $expected_priority ] as $action ) { 38 | $action_function = $action['function']; 39 | if ( is_array( $action_function ) ) { 40 | if ( $action_function[0] instanceof $class_type ) { 41 | $hooked_methods[] = $action_function[1]; 42 | } 43 | } 44 | } 45 | 46 | $this->assertNotEmpty( $hooked_methods, "No methods on an instance of $class_type hooked to $action_name" ); 47 | 48 | $this->assertContains( $method_name, $hooked_methods, "$method_name for $class_type class not hooked to $action_name" ); 49 | } 50 | 51 | /** 52 | * Verify admin_enqueue_scripts action is added for scripts, at priority 10. 53 | */ 54 | public function test_action_admin_enqueue_scripts_scripts() { 55 | 56 | $action_name = 'admin_enqueue_scripts'; 57 | $expected_priority = 10; 58 | $class_type = Admin::class; 59 | $method_name = 'enqueue_scripts'; 60 | 61 | global $wp_filter; 62 | 63 | $this->assertArrayHasKey( $action_name, $wp_filter, "$method_name definitely not hooked to $action_name" ); 64 | 65 | $actions_hooked = $wp_filter[ $action_name ]; 66 | 67 | $this->assertArrayHasKey( $expected_priority, $actions_hooked, "$method_name definitely not hooked to $action_name priority $expected_priority" ); 68 | 69 | $hooked_method = null; 70 | foreach ( $actions_hooked[ $expected_priority ] as $action ) { 71 | $action_function = $action['function']; 72 | if ( is_array( $action_function ) ) { 73 | if ( $action_function[0] instanceof $class_type ) { 74 | $hooked_method = $action_function[1]; 75 | } 76 | } 77 | } 78 | 79 | $this->assertNotNull( $hooked_method, "No methods on an instance of $class_type hooked to $action_name" ); 80 | 81 | $this->assertEquals( $method_name, $hooked_method, "Unexpected method name for $class_type class hooked to $action_name" ); 82 | } 83 | 84 | /** 85 | * Verify action to call load textdomain is added. 86 | */ 87 | public function test_action_plugins_loaded_load_plugin_textdomain() { 88 | 89 | $action_name = 'plugins_loaded'; 90 | $expected_priority = 10; 91 | $class_type = I18n::class; 92 | $method_name = 'load_plugin_textdomain'; 93 | 94 | global $wp_filter; 95 | 96 | $this->assertArrayHasKey( $action_name, $wp_filter, "$method_name definitely not hooked to $action_name" ); 97 | 98 | $actions_hooked = $wp_filter[ $action_name ]; 99 | 100 | $this->assertArrayHasKey( $expected_priority, $actions_hooked, "$method_name definitely not hooked to $action_name priority $expected_priority" ); 101 | 102 | $hooked_method = null; 103 | foreach ( $actions_hooked[ $expected_priority ] as $action ) { 104 | $action_function = $action['function']; 105 | if ( is_array( $action_function ) ) { 106 | if ( $action_function[0] instanceof $class_type ) { 107 | $hooked_method = $action_function[1]; 108 | } 109 | } 110 | } 111 | 112 | $this->assertNotNull( $hooked_method, "No methods on an instance of $class_type hooked to $action_name" ); 113 | 114 | $this->assertEquals( $method_name, $hooked_method, "Unexpected method name for $class_type class hooked to $action_name" ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/integration/wp-includes/class-functions-integration-Test.php: -------------------------------------------------------------------------------- 1 | setLogger( $test_logger ); 28 | 29 | $closure = function () { 30 | // phpcs:disable WordPress.WP.DeprecatedFunctions.documentation_linkFound 31 | documentation_link(); 32 | }; 33 | 34 | \BH_WP_Logger_Test_Plugin\run_closure_in_plugin( $closure ); 35 | 36 | $this->assertTrue( $test_logger->hasWarningThatContains( 'documentation_link' ) ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/integration/wp-includes/class-i18n-Test.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace BH_WP_Logger\WP_Includes; 10 | 11 | /** 12 | * @coversNothing 13 | * @see I18n 14 | */ 15 | class BH_WP_Logger_I18n_Test extends \Codeception\TestCase\WPTestCase { 16 | 17 | /** 18 | * AFAICT, this will fail until a translation has been added. 19 | * 20 | * @see load_plugin_textdomain() 21 | * @see https://gist.github.com/GaryJones/c8259da3a4501fd0648f19beddce0249 22 | */ 23 | public function test_load_plugin_textdomain() { 24 | 25 | $this->markTestSkipped( 'Needs one translation before test might pass.' ); 26 | 27 | global $plugin_root_dir; 28 | 29 | $this->assertTrue( file_exists( $plugin_root_dir . '/languages/' ), '/languages/ folder does not exist.' ); 30 | 31 | // Seems to fail because there are no translations to load. 32 | $this->assertTrue( is_textdomain_loaded( 'bh-wp-logger' ), 'i18n text domain not loaded.' ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit or integration tests. 4 | 5 | actor: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \Helper\Unit 10 | step_decorators: ~ 11 | bootstrap: _bootstrap.php 12 | -------------------------------------------------------------------------------- /tests/unit/_bootstrap.php: -------------------------------------------------------------------------------- 1 | makeEmpty( 32 | Logger_Settings_Interface::class, 33 | array( 34 | 'get_plugin_slug' => 'test', 35 | ) 36 | ); 37 | $api = $this->makeEmpty( 38 | API_Interface::class, 39 | array() 40 | ); 41 | 42 | \WP_Mock::userFunction( 43 | 'is_admin', 44 | array( 45 | 'return' => true, 46 | 'times' => 1, 47 | ) 48 | ); 49 | 50 | \WP_Mock::userFunction( 51 | 'delete_option', 52 | array( 53 | 'args' => array( 'test-recent-error-data' ), 54 | 'times' => 1, 55 | ) 56 | ); 57 | 58 | \WP_Mock::userFunction( 59 | 'sanitize_key', 60 | array( 61 | 'return_arg' => true, 62 | 'times' => 1, 63 | ) 64 | ); 65 | 66 | $_GET['page'] = 'test-logs'; 67 | 68 | $sut = new Admin_Notices( $api, $settings, $logger ); 69 | 70 | $sut->admin_notices(); 71 | } 72 | 73 | 74 | /** 75 | * @covers ::admin_notices 76 | */ 77 | public function test_return_early_when_not_in_admin_or_ajax(): void { 78 | 79 | $logger = new ColorLogger(); 80 | $settings = $this->makeEmpty( 81 | Logger_Settings_Interface::class, 82 | array( 83 | 'get_plugin_slug' => Expected::never(), 84 | ) 85 | ); 86 | $api = $this->makeEmpty( 87 | API_Interface::class, 88 | array() 89 | ); 90 | 91 | \WP_Mock::userFunction( 92 | 'is_admin', 93 | array( 94 | 'return' => false, 95 | 'times' => 1, 96 | ) 97 | ); 98 | 99 | \WP_Mock::userFunction( 100 | 'wp_doing_ajax', 101 | array( 102 | 'return' => false, 103 | 'times' => 1, 104 | ) 105 | ); 106 | 107 | $sut = new Admin_Notices( $api, $settings, $logger ); 108 | 109 | $sut->admin_notices(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/unit/admin/class-ajax-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( 31 | API_Interface::class, 32 | array( 33 | 'delete_all_logs' => Expected::once( 34 | function () { 35 | return array( 'success' => true ); } 36 | ), 37 | ) 38 | ); 39 | 40 | $settings = $this->makeEmpty( 41 | Logger_Settings_Interface::class, 42 | array( 43 | 'get_plugin_slug' => Expected::once( 44 | function () { 45 | return 'test-plugin-slug';} 46 | ), 47 | ) 48 | ); 49 | 50 | \WP_Mock::userFunction( 51 | 'wp_verify_nonce', 52 | array( 53 | 'args' => array( 'nonce_value', 'bh-wp-logger-delete' ), 54 | 'times' => 1, 55 | 'return' => true, 56 | ) 57 | ); 58 | 59 | \WP_Mock::userFunction( 60 | 'sanitize_key', 61 | array( 62 | 'return_arg' => true, 63 | ) 64 | ); 65 | 66 | \WP_Mock::userFunction( 67 | 'wp_send_json_success', 68 | array( 69 | 'times' => 1, 70 | ) 71 | ); 72 | 73 | $sut = new AJAX( $api, $settings ); 74 | 75 | // $_POST['action'] is handled by WordPress, not by our function. 76 | $_POST['plugin_slug'] = 'test-plugin-slug'; 77 | $_POST['_wpnonce'] = 'nonce_value'; 78 | 79 | $sut->delete_all(); 80 | } 81 | 82 | 83 | /** 84 | * @covers ::delete 85 | * @covers ::__construct 86 | */ 87 | public function test_delete(): void { 88 | 89 | $api = $this->makeEmpty( 90 | API_Interface::class, 91 | array( 92 | 'delete_log' => Expected::once( 93 | function ( string $ymd_date ) { 94 | return array( 'success' => true ); } 95 | ), 96 | ) 97 | ); 98 | 99 | $settings = $this->makeEmpty( 100 | Logger_Settings_Interface::class, 101 | array( 102 | 'get_plugin_slug' => Expected::once( 103 | function () { 104 | return 'test-plugin-slug'; 105 | } 106 | ), 107 | ) 108 | ); 109 | 110 | \WP_Mock::userFunction( 111 | 'wp_verify_nonce', 112 | array( 113 | 'args' => array( 'nonce_value', 'bh-wp-logger-delete' ), 114 | 'times' => 1, 115 | 'return' => true, 116 | ) 117 | ); 118 | 119 | \WP_Mock::userFunction( 120 | 'sanitize_key', 121 | array( 122 | 'return_arg' => true, 123 | ) 124 | ); 125 | 126 | \WP_Mock::userFunction( 127 | 'wp_send_json_success', 128 | array( 129 | 'times' => 1, 130 | ) 131 | ); 132 | 133 | $sut = new AJAX( $api, $settings ); 134 | 135 | // $_POST['action'] is handled by WordPress, not by our function. 136 | $_POST['plugin_slug'] = 'test-plugin-slug'; 137 | $_POST['_wpnonce'] = 'nonce_value'; 138 | $_POST['date_to_delete'] = '2022-03-02'; 139 | 140 | $sut->delete(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/unit/admin/class-logs-page-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( API_Interface::class ); 35 | $settings = $this->makeEmpty( 36 | Logger_Settings_Interface::class, 37 | array( 38 | 'get_plugin_slug' => 'test-enqueue-styles', 39 | ) 40 | ); 41 | // TODO: It would be nice to use ColorLogger here with this. 42 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 43 | 44 | $screen = new \stdClass(); 45 | $screen->id = 'admin_page_test-enqueue-styles-logs'; 46 | 47 | // Return any old url. 48 | \WP_Mock::userFunction( 49 | 'get_current_screen', 50 | array( 51 | 'return' => $screen, 52 | 'times' => 1, 53 | ) 54 | ); 55 | 56 | global $plugin_root_dir; 57 | // Return any old url. 58 | \WP_Mock::userFunction( 59 | 'plugin_dir_url', 60 | array( 61 | 'return' => $plugin_root_dir . '/admin/', 62 | 'times' => 1, 63 | ) 64 | ); 65 | 66 | $version = '1.0.0'; 67 | 68 | \WP_Mock::userFunction( 69 | 'wp_enqueue_style', 70 | array( 71 | 'args' => array( 'test-enqueue-styles-logs', \WP_Mock\Functions::type( 'string' ), array(), $version, 'all' ), 72 | 'times' => 1, 73 | ) 74 | ); 75 | 76 | $sut = new Logs_Page( $api, $settings, $logger ); 77 | 78 | $sut->enqueue_styles(); 79 | } 80 | 81 | 82 | 83 | /** 84 | * Verifies enqueue_styles() calls wp_enqueue_style() with appropriate parameters. 85 | * Verifies the .css file exists. 86 | * 87 | * @covers ::enqueue_scripts 88 | * @see wp_enqueue_style() 89 | */ 90 | public function test_enqueue_scripts() { 91 | 92 | $api = $this->makeEmpty( API_Interface::class ); 93 | $settings = $this->makeEmpty( 94 | Logger_Settings_Interface::class, 95 | array( 96 | 'get_plugin_slug' => 'test-enqueue-styles', 97 | ) 98 | ); 99 | // TODO: It would be nice to use ColorLogger here with this. 100 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 101 | 102 | $screen = new \stdClass(); 103 | $screen->id = 'admin_page_test-enqueue-styles-logs'; 104 | 105 | // Return any old url. 106 | \WP_Mock::userFunction( 107 | 'get_current_screen', 108 | array( 109 | 'return' => $screen, 110 | 'times' => 1, 111 | ) 112 | ); 113 | 114 | global $plugin_root_dir; 115 | // Return any old url. 116 | \WP_Mock::userFunction( 117 | 'plugin_dir_url', 118 | array( 119 | 'return' => $plugin_root_dir . '/admin/', 120 | 'times' => 1, 121 | ) 122 | ); 123 | 124 | $handle = 'bh-wp-logger-admin-logs-page-test-enqueue-styles'; 125 | $version = '1.1.0'; 126 | 127 | \WP_Mock::userFunction( 128 | 'wp_enqueue_script', 129 | array( 130 | 'args' => array( 131 | $handle, 132 | \WP_Mock\Functions::type( 'string' ), 133 | \WP_Mock\Functions::type( 'array' ), // 2023-03: jquery, renderjson, colresizable 134 | $version, 135 | true, 136 | ), 137 | 'times' => 1, 138 | ) 139 | ); 140 | 141 | \WP_Mock::userFunction( 142 | 'wp_enqueue_script', 143 | array( 144 | 'args' => array( 145 | 'renderjson', 146 | \WP_Mock\Functions::type( 'string' ), 147 | \WP_Mock\Functions::type( 'array' ), 148 | \WP_Mock\Functions::type( 'string' ), // version. 149 | true, 150 | ), 151 | 'times' => 1, 152 | ) 153 | ); 154 | 155 | \WP_Mock::userFunction( 156 | 'wp_enqueue_script', 157 | array( 158 | 'args' => array( 159 | 'colresizable', 160 | \WP_Mock\Functions::type( 'string' ), 161 | \WP_Mock\Functions::type( 'array' ), 162 | \WP_Mock\Functions::type( 'string' ), // version. 163 | true, 164 | ), 165 | 'times' => 1, 166 | ) 167 | ); 168 | 169 | \WP_Mock::passthruFunction( 'plugin_dir_url' ); 170 | 171 | $sut = new Logs_Page( $api, $settings, $logger ); 172 | 173 | $sut->enqueue_scripts(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/unit/admin/class-plugin-installer-unit-Test.php: -------------------------------------------------------------------------------- 1 | 0, 41 | ) 42 | ); 43 | 44 | $settings = $this->makeEmpty( 45 | Logger_Settings_Interface::class, 46 | array( 47 | 'get_plugin_basename' => Expected::once( 'bh-wp-my-plugin/bh-wp-my-plugin.php' ), 48 | 'get_plugin_slug' => Expected::once( 'bh-wp-my-plugin' ), 49 | 'get_plugin_name' => Expected::once( 'My Plugin' ), 50 | ) 51 | ); 52 | 53 | $sut = new Plugin_Installer( $settings ); 54 | 55 | $result = $sut->add_logs_link( array(), null, 'bh-wp-my-plugin/bh-wp-my-plugin.php' ); 56 | 57 | $this->assertIsArray( $result ); 58 | 59 | $link_html = array_pop( $result ); 60 | 61 | $this->assertStringContainsString( 'Go to My Plugin logs', $link_html ); 62 | 63 | $this->assertStringContainsString( 'href="/admin.php?page=bh-wp-my-plugin', $link_html ); 64 | } 65 | 66 | 67 | /** 68 | * @covers ::add_logs_link 69 | */ 70 | public function test_return_early_for_other_plugins(): void { 71 | 72 | $settings = $this->makeEmpty( 73 | Logger_Settings_Interface::class, 74 | array( 75 | 'get_plugin_basename' => Expected::once( 'bh-wp-my-plugin/bh-wp-my-plugin.php' ), 76 | 'get_plugin_slug' => Expected::never( 'bh-wp-my-plugin' ), 77 | 'get_plugin_name' => Expected::never( 'My Plugin' ), 78 | ) 79 | ); 80 | $logger = new ColorLogger(); 81 | 82 | $sut = new Plugin_Installer( $settings, $logger ); 83 | 84 | $result = $sut->add_logs_link( array(), null, 'any-other-plugin/any-other-plugin.php' ); 85 | 86 | $this->assertIsArray( $result ); 87 | 88 | $this->assertEmpty( $result ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/unit/admin/class-plugins-page-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( API_Interface::class ); 33 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 34 | 35 | $sut = new Plugins_Page( $api, $settings ); 36 | 37 | $this->assertInstanceOf( Plugins_Page::class, $sut ); 38 | } 39 | 40 | 41 | /** 42 | * @covers ::add_logs_action_link 43 | */ 44 | public function test_logs_link_added(): void { 45 | 46 | \WP_Mock::userFunction( 47 | 'admin_url', 48 | array( 49 | 'return_arg' => 0, 50 | ) 51 | ); 52 | 53 | $api = $this->makeEmpty( 54 | API_Interface::class, 55 | array( 56 | 'get_log_url' => 'admin.php?page=bh-wp-logger-test-plugin-logs', 57 | ) 58 | ); 59 | 60 | $settings = $this->makeEmpty( 61 | Logger_Settings_Interface::class, 62 | array( 'get_plugin_slug' => 'bh-wp-logger-test-plugin' ) 63 | ); 64 | $logger = new ColorLogger(); 65 | 66 | $sut = new Plugins_Page( $api, $settings, $logger ); 67 | 68 | // Return the default value when get_option is called. 69 | \WP_Mock::userFunction( 70 | 'get_option', 71 | array( 72 | 'return_arg' => 1, 73 | ) 74 | ); 75 | 76 | $result = $sut->add_logs_action_link( array(), '', array(), '' ); 77 | 78 | $this->assertIsArray( $result ); 79 | 80 | $link_html = $result[0]; 81 | 82 | $this->assertStringContainsString( 'Logs', $link_html ); 83 | 84 | // To distinguish this from the later test. 85 | $this->assertStringNotContainsString( '', $link_html ); 86 | 87 | $this->assertStringContainsString( 'href="admin.php?page=bh-wp-logger-test-plugin-logs', $link_html ); 88 | } 89 | 90 | 91 | /** 92 | * @covers ::add_logs_action_link 93 | */ 94 | public function test_logs_link_placed_before_deactivate(): void { 95 | 96 | \WP_Mock::userFunction( 97 | 'admin_url', 98 | array( 99 | 'return_arg' => 0, 100 | ) 101 | ); 102 | 103 | $api = $this->makeEmpty( 104 | API_Interface::class, 105 | array( 106 | 'get_log_url' => 'admin.php?page=bh-wp-logger-test-plugin-logs', 107 | ) 108 | ); 109 | 110 | $settings = $this->makeEmpty( 111 | Logger_Settings_Interface::class, 112 | array( 'get_plugin_slug' => 'bh-wp-logger-test-plugin' ) 113 | ); 114 | $logger = new ColorLogger(); 115 | 116 | $sut = new Plugins_Page( $api, $settings, $logger ); 117 | 118 | // Return the default value when get_option is called. 119 | \WP_Mock::userFunction( 120 | 'get_option', 121 | array( 122 | 'return_arg' => 1, 123 | ) 124 | ); 125 | 126 | $existing_links = array( 127 | 'Settings', 128 | 'Deactivate', 129 | ); 130 | 131 | $result = $sut->add_logs_action_link( $existing_links, '', array(), '' ); 132 | 133 | $this->assertStringContainsString( 'Settings', $result[0] ); 134 | $this->assertStringContainsString( 'Logs', $result[1] ); 135 | $this->assertStringContainsString( 'Deactivate', $result[2] ); 136 | } 137 | 138 | /** 139 | * @covers ::add_logs_action_link 140 | */ 141 | public function test_new_logs_get_highlighted(): void { 142 | 143 | \WP_Mock::userFunction( 144 | 'admin_url', 145 | array( 146 | 'return_arg' => 0, 147 | ) 148 | ); 149 | 150 | $api = $this->makeEmpty( 151 | API_Interface::class, 152 | array( 153 | // NB: the order here matters because the milliseconds difference make the test pass/fail. 154 | 'get_last_logs_view_time' => new DateTime(), 155 | 'get_last_log_time' => new DateTime(), 156 | ) 157 | ); 158 | 159 | $settings = $this->makeEmpty( 160 | Logger_Settings_Interface::class, 161 | array( 'get_plugin_slug' => 'bh-wp-logger-test-plugin' ) 162 | ); 163 | $logger = new ColorLogger(); 164 | 165 | // When the latest log time is more recent than the last logs view time... 166 | \WP_Mock::userFunction( 167 | 'get_option', 168 | array( 169 | 'args' => array( 'bh-wp-logger-test-plugin-last-log-time', 0 ), 170 | 'return' => time(), 171 | ) 172 | ); 173 | \WP_Mock::userFunction( 174 | 'get_option', 175 | array( 176 | 'args' => array( 'bh-wp-logger-test-plugin-last-logs-view-time', 0 ), 177 | 'return' => time() - 60 * 60, // HOUR_IN_SECONDS. 178 | ) 179 | ); 180 | 181 | $sut = new Plugins_Page( $api, $settings, $logger ); 182 | 183 | $result = $sut->add_logs_action_link( array(), '', array(), '' ); 184 | 185 | $this->assertIsArray( $result ); 186 | 187 | $link_html = $result[0]; 188 | 189 | $this->assertStringContainsString( '', $link_html ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/unit/api/class-bh-wp-psr-logger-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( Logger_Settings_Interface::class ); 32 | 33 | $sut = new BH_WP_PSR_Logger( $settings, $logger ); 34 | 35 | $exception = new \Exception( 'Exception message', 123 ); 36 | 37 | \WP_Mock::userFunction( 38 | 'update_option' 39 | ); 40 | 41 | \WP_Mock::userFunction( 42 | 'delete_transient' 43 | ); 44 | 45 | $sut->error( 'Error', array( 'exception' => $exception ) ); 46 | 47 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 48 | $logged_exception = $logger->recordsByLevel['error'][0]['context']['exception']; 49 | 50 | $this->assertArrayHasKey( 'class', $logged_exception ); 51 | 52 | $this->assertArrayHasKey( 'message', $logged_exception ); 53 | $this->assertEquals( 'Exception message', $logged_exception['message'] ); 54 | 55 | $this->assertArrayHasKey( 'properties', $logged_exception ); 56 | $this->assertEquals( 123, $logged_exception['properties']['code'] ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/unit/class-logger-unit-Test.php: -------------------------------------------------------------------------------- 1 | array( 'test-logger/test-logger.php' => array() ), 43 | ) 44 | ); 45 | \WP_Mock::userFunction( 46 | 'plugin_basename', 47 | array( 48 | 'return' => 'test-logger/test-logger.php', 49 | ) 50 | ); 51 | \WP_Mock::userFunction( 52 | 'get_option', 53 | array( 54 | 'return_arg' => 1, 55 | ) 56 | ); 57 | \WP_Mock::passthruFunction( 'sanitize_key' ); 58 | 59 | $logger = Logger::instance(); 60 | 61 | $this->assertInstanceOf( BH_WP_PSR_Logger::class, $logger ); 62 | $this->assertInstanceOf( LoggerInterface::class, $logger ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/unit/class-plugin-unit-Test.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace BH_WP_Logger_Test_Plugin; 10 | 11 | use BH_WP_Logger_Test_Plugin\WP_Includes\BH_WP_Logger_Test_Plugin; 12 | 13 | /** 14 | * Class Plugin_WP_Mock_Test 15 | * 16 | * @coversNothing 17 | */ 18 | class Plugin_Unit_Test extends \Codeception\Test\Unit { 19 | 20 | 21 | protected function setup(): void { 22 | \WP_Mock::setUp(); 23 | } 24 | 25 | protected function tearDown(): void { 26 | \WP_Mock::tearDown(); 27 | \Patchwork\restoreAll(); 28 | } 29 | 30 | /** 31 | * Verifies the plugin initialization. 32 | */ 33 | public function test_plugin_include(): void { 34 | 35 | // Prevents code-coverage counting, and removes the need to define the WordPress functions that are used in that class. 36 | \Patchwork\redefine( 37 | array( BH_WP_Logger_Test_Plugin::class, '__construct' ), 38 | function ( $settings, $logger ) {} 39 | ); 40 | 41 | $plugin_root_dir = dirname( __DIR__, 2 ) . '/test-plugin'; 42 | 43 | \WP_Mock::userFunction( 44 | 'plugin_dir_path', 45 | array( 46 | 'args' => array( \WP_Mock\Functions::type( 'string' ) ), 47 | 'return' => $plugin_root_dir . '/', 48 | ) 49 | ); 50 | 51 | \WP_Mock::userFunction( 52 | 'plugin_basename', 53 | array( 54 | 'args' => array( \WP_Mock\Functions::type( 'string' ) ), 55 | 'return' => 'bh-wp-logger-test-plugin/bh-wp-logger-test-plugin.php', 56 | ) 57 | ); 58 | 59 | \WP_Mock::userFunction( 60 | 'get_option', 61 | array( 62 | 'args' => array( 'active_plugins' ), 63 | 'return' => array(), 64 | ) 65 | ); 66 | 67 | \WP_Mock::userFunction( 68 | 'is_admin', 69 | array( 70 | 'return' => false, 71 | ) 72 | ); 73 | 74 | \WP_Mock::userFunction( 75 | 'get_current_user_id' 76 | ); 77 | 78 | \WP_Mock::userFunction( 79 | 'wp_normalize_path', 80 | array( 81 | 'return_arg' => true, 82 | ) 83 | ); 84 | 85 | \WP_Mock::userFunction( 86 | 'get_option', 87 | array( 88 | 'args' => array( 'active_plugins', \WP_Mock\Functions::type( 'array' ) ), 89 | 'return' => array( 'woocommerce/woocommerce.php' ), 90 | ) 91 | ); 92 | 93 | \WP_Mock::userFunction( 94 | 'did_action', 95 | array( 96 | 'return' => false, 97 | ) 98 | ); 99 | 100 | \WP_Mock::userFunction( 101 | 'add_action', 102 | array( 103 | 'return' => false, 104 | ) 105 | ); 106 | 107 | ob_start(); 108 | 109 | include $plugin_root_dir . '/bh-wp-logger-test-plugin.php'; 110 | 111 | $printed_output = ob_get_contents(); 112 | 113 | ob_end_clean(); 114 | 115 | $this->assertEmpty( $printed_output ); 116 | 117 | $this->assertArrayHasKey( 'bh_wp_logger_test_plugin', $GLOBALS ); 118 | 119 | $this->assertInstanceOf( BH_WP_Logger_Test_Plugin::class, $GLOBALS['bh_wp_logger_test_plugin'] ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/unit/patchwork.json: -------------------------------------------------------------------------------- 1 | { 2 | "redefinable-internals": [ 3 | "error_get_last" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/unit/php/class-php-error-handler-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( API_Interface::class ); 25 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 26 | $logger = new ColorLogger(); 27 | 28 | $sut = new PHP_Error_Handler( $api, $settings, $logger ); 29 | 30 | $method = new \ReflectionMethod( PHP_Error_Handler::class, 'errno_to_psr3' ); 31 | $method->setAccessible( true ); 32 | 33 | $result = $method->invoke( $sut, $from ); 34 | 35 | $this->assertEquals( $to, $result ); 36 | } 37 | 38 | public function error_levels(): array { 39 | return array( 40 | array( E_ERROR, LogLevel::ERROR ), 41 | array( E_DEPRECATED, LogLevel::NOTICE ), 42 | ); 43 | } 44 | 45 | /** 46 | * @covers ::errno_to_psr3 47 | */ 48 | public function test_php_error_level_to_psr_level_unknown(): void { 49 | 50 | $api = $this->makeEmpty( API_Interface::class ); 51 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 52 | $logger = new ColorLogger(); 53 | 54 | $sut = new PHP_Error_Handler( $api, $settings, $logger ); 55 | 56 | $method = new \ReflectionMethod( PHP_Error_Handler::class, 'errno_to_psr3' ); 57 | $method->setAccessible( true ); 58 | 59 | $result = $method->invoke( $sut, 918273645 ); 60 | 61 | $this->assertEquals( LogLevel::ERROR, $result ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/unit/php/class-php-shutdown-handler-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( API_Interface::class ); 30 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 31 | $logger = new ColorLogger(); 32 | 33 | $sut = new PHP_Shutdown_Handler( $api, $settings, $logger ); 34 | 35 | $this->assertInstanceOf( PHP_Shutdown_Handler::class, $sut ); 36 | } 37 | 38 | /** 39 | * @covers ::init 40 | */ 41 | public function test_register_handler(): void { 42 | 43 | $api = $this->makeEmpty( API_Interface::class ); 44 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 45 | $logger = new ColorLogger(); 46 | 47 | $sut = new PHP_Shutdown_Handler( $api, $settings, $logger ); 48 | 49 | $e = null; 50 | try { 51 | $sut->init(); 52 | } catch ( \Exception $exception ) { 53 | $this->fail(); 54 | } 55 | 56 | $this->assertNull( $e ); 57 | } 58 | 59 | /** 60 | * @covers ::handle 61 | */ 62 | public function test_error(): void { 63 | 64 | $api = $this->makeEmpty( 65 | API_Interface::class, 66 | array( 67 | 'is_file_from_plugin' => true, 68 | ) 69 | ); 70 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 71 | $logger = new ColorLogger(); 72 | 73 | $sut = new PHP_Shutdown_Handler( $api, $settings, $logger ); 74 | 75 | \Patchwork\redefine( 76 | 'error_get_last', 77 | function () { 78 | return array( 79 | 'file' => __FILE__, 80 | 'message' => 'error message', 81 | ); 82 | } 83 | ); 84 | 85 | $sut->handle(); 86 | 87 | $this->assertTrue( $logger->hasErrorThatContains( 'error message' ) ); 88 | } 89 | 90 | /** 91 | * @covers ::handle 92 | */ 93 | public function test_no_error(): void { 94 | 95 | $api = $this->makeEmpty( API_Interface::class ); 96 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 97 | $logger = new ColorLogger(); 98 | 99 | $sut = new PHP_Shutdown_Handler( $api, $settings, $logger ); 100 | 101 | \Patchwork\redefine( 102 | 'error_get_last', 103 | function () { 104 | return null; 105 | } 106 | ); 107 | 108 | $sut->handle(); 109 | 110 | $this->assertFalse( $logger->hasErrorThatContains( 'error message' ) ); 111 | } 112 | 113 | 114 | /** 115 | * @covers ::handle 116 | */ 117 | public function test_error_wrong_file(): void { 118 | 119 | $api = $this->makeEmpty( 120 | API_Interface::class, 121 | array( 122 | 'is_file_from_plugin' => false, 123 | ) 124 | ); 125 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 126 | $logger = new ColorLogger(); 127 | 128 | $sut = new PHP_Shutdown_Handler( $api, $settings, $logger ); 129 | 130 | \Patchwork\redefine( 131 | 'error_get_last', 132 | function () { 133 | return array( 134 | 'file' => __FILE__, 135 | 'message' => 'error message', 136 | ); 137 | } 138 | ); 139 | 140 | $sut->handle(); 141 | 142 | $this->assertFalse( $logger->hasErrorThatContains( 'error message' ) ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/unit/private-uploads/class-url-is-public-unit-Test.php: -------------------------------------------------------------------------------- 1 | change_warning_message( $message, $url ); 23 | 24 | $this->assertStringContainsString( 'Please update your webserver configuration', $result ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/wp-includes/class-cron-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( API_Interface::class ); 33 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 34 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 35 | 36 | $cron = new Cron( $api, $settings, $logger ); 37 | 38 | $this->assertInstanceOf( Cron::class, $cron ); 39 | } 40 | 41 | /** 42 | * @covers ::register_delete_logs_cron_job 43 | */ 44 | public function test_register_delete_logs_cron_job(): void { 45 | 46 | $api = $this->makeEmpty( API_Interface::class ); 47 | $settings = $this->makeEmpty( 48 | Logger_Settings_Interface::class, 49 | array( 50 | 'get_plugin_slug' => 'test-plugin', 51 | ) 52 | ); 53 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 54 | 55 | $cron = new Cron( $api, $settings, $logger ); 56 | 57 | $cron_hook = 'delete_logs_test-plugin'; 58 | 59 | \WP_Mock::userFunction( 60 | 'wp_get_scheduled_event', 61 | array( 62 | 'args' => array( $cron_hook ), 63 | 'return' => false, 64 | 'times' => 1, 65 | ) 66 | ); 67 | 68 | \WP_Mock::userFunction( 69 | 'wp_schedule_event', 70 | array( 71 | 'args' => array( \WP_Mock\Functions::type( 'int' ), 'daily', $cron_hook ), 72 | 'times' => 1, 73 | ) 74 | ); 75 | 76 | $cron->register_delete_logs_cron_job(); 77 | } 78 | 79 | /** 80 | * @covers ::register_delete_logs_cron_job 81 | */ 82 | public function test_do_not_schedule_if_already_scheduled(): void { 83 | $api = $this->makeEmpty( API_Interface::class ); 84 | $settings = $this->makeEmpty( 85 | Logger_Settings_Interface::class, 86 | array( 87 | 'get_plugin_slug' => 'test-plugin', 88 | ) 89 | ); 90 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 91 | 92 | $cron = new Cron( $api, $settings, $logger ); 93 | 94 | $cron_hook = 'delete_logs_test-plugin'; 95 | 96 | \WP_Mock::userFunction( 97 | 'wp_get_scheduled_event', 98 | array( 99 | 'args' => array( $cron_hook ), 100 | 'return' => true, 101 | 'times' => 1, 102 | ) 103 | ); 104 | 105 | \WP_Mock::userFunction( 106 | 'wp_schedule_event', 107 | array( 108 | 'times' => 0, 109 | ) 110 | ); 111 | 112 | $cron->register_delete_logs_cron_job(); 113 | } 114 | 115 | /** 116 | * @covers ::register_delete_logs_cron_job 117 | */ 118 | public function test_do_not_schedule_for_woocommerce_logger(): void { 119 | $api = $this->makeEmpty( API_Interface::class ); 120 | $settings = $this->makeEmpty( 121 | WooCommerce_Logger_Settings_Interface::class, 122 | array( 123 | 'get_plugin_slug' => 'test-plugin', 124 | ) 125 | ); 126 | $logger = $this->makeEmpty( 127 | BH_WP_PSR_Logger::class, 128 | array( 129 | 'get_logger' => $this->makeEmpty( WC_PSR_Logger::class ), 130 | ) 131 | ); 132 | 133 | $cron = new Cron( $api, $settings, $logger ); 134 | 135 | $cron_hook = 'delete_logs_test-plugin'; 136 | 137 | \WP_Mock::userFunction( 138 | 'wp_get_scheduled_event', 139 | array( 140 | 'args' => array( $cron_hook ), 141 | 'return' => true, 142 | 'times' => 0, 143 | ) 144 | ); 145 | 146 | \WP_Mock::userFunction( 147 | 'wp_schedule_event', 148 | array( 149 | 'times' => 0, 150 | ) 151 | ); 152 | 153 | $cron->register_delete_logs_cron_job(); 154 | } 155 | 156 | /** 157 | * @covers ::delete_old_logs 158 | */ 159 | public function test_execute_cron(): void { 160 | 161 | $api = $this->makeEmpty( 162 | API_Interface::class, 163 | array( 164 | 'delete_old_logs' => Expected::once(), 165 | ) 166 | ); 167 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 168 | $color_logger = new ColorLogger(); 169 | 170 | $logger = $this->makeEmpty( 171 | BH_WP_PSR_Logger::class, 172 | array( 173 | 'logger' => $color_logger, 174 | 'get_logger' => $color_logger, 175 | 'debug' => function ( $message, $context ) use ( $color_logger ) { 176 | $color_logger->debug( $message, $context ); 177 | }, 178 | ) 179 | ); 180 | 181 | \WP_Mock::userFunction( 182 | 'current_action', 183 | array( 184 | 'return' => 'testing_cron', 185 | 'times' => 1, 186 | ) 187 | ); 188 | 189 | $cron = new Cron( $api, $settings, $logger ); 190 | 191 | $cron->delete_old_logs(); 192 | 193 | $this->assertTrue( $color_logger->hasDebug( 'Executing testing_cron cron job.' ) ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /tests/unit/wp-includes/class-plugin-logger-actions-unit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( BH_WP_PSR_Logger::class ); 37 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 38 | $api = $this->makeEmpty( API_Interface::class ); 39 | new Plugin_Logger_Actions( $api, $settings, $logger ); 40 | } 41 | 42 | /** 43 | * @covers ::add_plugins_page_hooks 44 | */ 45 | public function test_plugins_page_hooks(): void { 46 | 47 | $basename = 'test-plugin/test-plugin.php'; 48 | 49 | \WP_Mock::expectFilterAdded( 50 | "plugin_action_links_{$basename}", 51 | array( new AnyInstance( Plugins_Page::class ), 'add_logs_action_link' ), 52 | 99, 53 | 4 54 | ); 55 | 56 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 57 | $settings = $this->makeEmpty( 58 | Logger_Settings_Interface::class, 59 | array( 60 | 'get_plugin_basename' => $basename, 61 | ) 62 | ); 63 | $api = $this->makeEmpty( API_Interface::class ); 64 | new Plugin_Logger_Actions( $api, $settings, $logger ); 65 | } 66 | 67 | 68 | /** 69 | * @covers ::add_plugin_installer_page_hooks 70 | */ 71 | public function test_plugins_installer_page_hooks(): void { 72 | 73 | $basename = 'test-plugin/test-plugin.php'; 74 | 75 | \WP_Mock::expectFilterAdded( 76 | 'install_plugin_complete_actions', 77 | array( new AnyInstance( Plugin_Installer::class ), 'add_logs_link' ), 78 | 99, 79 | 3 80 | ); 81 | 82 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 83 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 84 | $api = $this->makeEmpty( API_Interface::class ); 85 | new Plugin_Logger_Actions( $api, $settings, $logger ); 86 | } 87 | 88 | /** 89 | * @covers ::add_error_handler_hooks 90 | */ 91 | public function test_add_error_handler_hooks(): void { 92 | 93 | $api = $this->makeEmpty( API_Interface::class ); 94 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 95 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 96 | 97 | \WP_Mock::expectActionAdded( 98 | 'plugins_loaded', 99 | array( new AnyInstance( PHP_Error_Handler::class ), 'init' ), 100 | 2 101 | ); 102 | 103 | \WP_Mock::expectActionAdded( 104 | 'plugins_loaded', 105 | array( new AnyInstance( PHP_Shutdown_Handler::class ), 'init' ), 106 | 2 107 | ); 108 | 109 | new Plugin_Logger_Actions( $api, $settings, $logger ); 110 | } 111 | 112 | /** 113 | * @covers ::add_wordpress_error_handling_hooks 114 | */ 115 | public function test_add_wordpress_error_handling_hooks(): void { 116 | 117 | \WP_Mock::expectActionAdded( 118 | 'deprecated_function_run', 119 | array( new AnyInstance( Functions::class ), 'log_deprecated_functions_only_once_per_day' ), 120 | 10, 121 | 3 122 | ); 123 | 124 | \WP_Mock::expectActionAdded( 125 | 'deprecated_argument_run', 126 | array( new AnyInstance( Functions::class ), 'log_deprecated_arguments_only_once_per_day' ), 127 | 10, 128 | 3 129 | ); 130 | 131 | \WP_Mock::expectActionAdded( 132 | 'doing_it_wrong_run', 133 | array( new AnyInstance( Functions::class ), 'log_doing_it_wrong_only_once_per_day' ), 134 | 10, 135 | 3 136 | ); 137 | 138 | \WP_Mock::expectActionAdded( 139 | 'deprecated_hook_run', 140 | array( new AnyInstance( Functions::class ), 'log_deprecated_hook_only_once_per_day' ), 141 | 10, 142 | 4 143 | ); 144 | 145 | $api = $this->makeEmpty( API_Interface::class ); 146 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 147 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 148 | 149 | new class($api, $settings, $logger ) extends Plugin_Logger_Actions { 150 | protected function is_wp_debug(): bool { 151 | return true; 152 | } 153 | }; 154 | } 155 | 156 | /** 157 | * @covers ::add_cron_hooks 158 | */ 159 | public function test_add_cron_hooks(): void { 160 | 161 | \WP_Mock::expectActionAdded( 162 | 'init', 163 | array( new AnyInstance( Cron::class ), 'register_delete_logs_cron_job' ) 164 | ); 165 | \WP_Mock::expectActionAdded( 166 | 'delete_logs_plugin-slug', 167 | array( new AnyInstance( Cron::class ), 'delete_old_logs' ) 168 | ); 169 | 170 | $api = $this->makeEmpty( API_Interface::class ); 171 | $settings = $this->makeEmpty( 172 | Logger_Settings_Interface::class, 173 | array( 174 | 'get_plugin_slug' => 'plugin-slug', 175 | ) 176 | ); 177 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 178 | 179 | new Plugin_Logger_Actions( $api, $settings, $logger ); 180 | } 181 | 182 | 183 | /** 184 | * @covers ::add_private_uploads_hooks 185 | */ 186 | public function test_add_private_uploads_hooks(): void { 187 | 188 | \WP_Mock::expectFilterAdded( 189 | 'bh_wp_private_uploads_url_is_public_warning_plugin-slug_logger', 190 | array( new AnyInstance( URL_Is_Public::class ), 'change_warning_message' ), 191 | 10, 192 | 2 193 | ); 194 | 195 | $api = $this->makeEmpty( API_Interface::class ); 196 | $settings = $this->makeEmpty( 197 | Logger_Settings_Interface::class, 198 | array( 199 | 'get_plugin_slug' => 'plugin-slug', 200 | ) 201 | ); 202 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 203 | 204 | new Plugin_Logger_Actions( $api, $settings, $logger ); 205 | } 206 | 207 | 208 | /** 209 | * @covers ::define_init_hooks 210 | */ 211 | public function test_init_hooks(): void { 212 | 213 | \WP_Mock::expectActionAdded( 214 | 'init', 215 | array( new AnyInstance( Init::class ), 'maybe_download_log' ) 216 | ); 217 | 218 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 219 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 220 | $api = $this->makeEmpty( API_Interface::class ); 221 | new Plugin_Logger_Actions( $api, $settings, $logger ); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/wpunit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit or integration tests that require WordPress functions and classes. 4 | 5 | actor: WpunitTester 6 | modules: 7 | enabled: 8 | - WPLoader 9 | - \Helper\Wpunit 10 | config: 11 | WPLoader: 12 | wpRootFolder: "%WP_ROOT_FOLDER%" 13 | dbName: "%TEST_DB_NAME%" 14 | dbHost: "%TEST_DB_HOST%" 15 | dbUser: "%TEST_DB_USER%" 16 | dbPassword: "%TEST_DB_PASSWORD%" 17 | tablePrefix: "%TEST_TABLE_PREFIX%" 18 | domain: "%TEST_SITE_WP_DOMAIN%" 19 | adminEmail: "%TEST_SITE_ADMIN_EMAIL%" 20 | title: "bh-wp-logger" 21 | plugins: ["woocommerce/woocommerce.php"] 22 | activatePlugins: ["woocommerce/woocommerce.php"] 23 | bootstrap: _bootstrap.php 24 | -------------------------------------------------------------------------------- /tests/wpunit/_bootstrap.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete(); 20 | 21 | // 2021-09-13T22:05:11Z UTC 22 | 23 | $api = $this->makeEmpty( API_Interface::class ); 24 | $settings = $this->makeEmpty( 25 | Logger_Settings_Interface::class, 26 | array( 27 | 'get_plugin_slug' => 'bh-wp-logger-tests', 28 | ) 29 | ); 30 | $logger = new ColorLogger(); 31 | 32 | $option_name = 'bh-wp-logger-tests-recent-error-data'; 33 | 34 | $sut = new Admin_Notices( $api, $settings, $logger ); 35 | 36 | $sut->admin_notices(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/wpunit/admin/class-log-list-table-wpunit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( API::class ); 26 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 27 | // TODO: It would be nice to use ColorLogger here with this. 28 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 29 | 30 | global $hook_suffix; 31 | $hook_suffix = ''; 32 | 33 | $sut = new Logs_List_Table( $api, $settings, $logger ); 34 | 35 | $message = "A log message with wp_user:{$user_1_id} and also with wp_user:{$user_2_id} in it."; 36 | 37 | $result = $sut->replace_wp_user_id_with_link( $message ); 38 | 39 | $this->assertStringContainsString( "user-edit.php?user_id={$user_1_id}", $result ); 40 | $this->assertStringContainsString( 'username_1', $result ); 41 | 42 | $this->assertStringContainsString( "user-edit.php?user_id={$user_2_id}", $result ); 43 | $this->assertStringContainsString( 'username_2', $result ); 44 | } 45 | 46 | /** 47 | * Let's try make the shop_order:123 replacement generic. 48 | * 49 | * @covers ::replace_post_type_id_with_link 50 | */ 51 | public function test_replace_custom_post_type_id(): void { 52 | 53 | $api = $this->makeEmpty( API_Interface::class ); 54 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 55 | $logger = $this->makeEmpty( BH_WP_PSR_Logger::class ); 56 | 57 | $post_id = wp_insert_attachment( array() ); 58 | 59 | $GLOBALS['hook_suffix'] = 'post'; 60 | 61 | $sut = new Logs_List_Table( $api, $settings, $logger ); 62 | 63 | $item = array( 64 | 'message' => "a string with custom `attachment:{$post_id}` post types with ids beside", 65 | ); 66 | 67 | // Output relies on the current user being able to edit_others_posts. 68 | add_filter( 69 | 'user_has_cap', 70 | function ( $allcaps, $caps, $args, $user ) { 71 | $allcaps['edit_others_posts'] = true; 72 | return $allcaps; 73 | }, 74 | 10, 75 | 4 76 | ); 77 | 78 | $result = $sut->column_default( $item, 'message' ); 79 | 80 | $expected = esc_html( "post.php?post={$post_id}&action=edit" ); 81 | 82 | $this->assertStringContainsString( $expected, $result ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/wpunit/api/class-bh-wp-psr-logger-wpunit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( 27 | Logger_Settings_Interface::class, 28 | array( 29 | 'get_plugin_slug' => 'plugin-slug', 30 | 'get_log_level' => 'info', 31 | ) 32 | ); 33 | 34 | $sut = new BH_WP_PSR_Logger( $setttings ); 35 | 36 | $logger = new class() extends ColorLogger { 37 | /** 38 | * Extend TestLogger to record context. 39 | * 40 | * @see TestLogger 41 | */ 42 | public $context = array(); 43 | public function log( $level, $message, array $context = array() ) { 44 | $this->context = $context; 45 | parent::log( $level, $message, $context ); 46 | } 47 | }; 48 | 49 | $sut->setLogger( $logger ); 50 | 51 | $sut->error( 'error log message' ); 52 | 53 | $this->assertArrayHasKey( 'debug_backtrace', $logger->context ); 54 | } 55 | 56 | /** 57 | * E.g. a library might often report "Trying to access array offset on value of type bool", which is a known 58 | * upstream issue that does not need to be reported/recorded. 59 | * 60 | * @covers ::log 61 | */ 62 | public function test_cancel_log_filter(): void { 63 | 64 | $logger = $this->makeEmpty( 65 | LoggerInterface::class, 66 | array( 67 | 'error' => Expected::never(), 68 | ) 69 | ); 70 | 71 | $setttings = $this->makeEmpty( 72 | Logger_Settings_Interface::class, 73 | array( 74 | 'get_plugin_slug' => 'plugin-slug', 75 | 'get_log_level' => 'info', 76 | ) 77 | ); 78 | 79 | $sut = new BH_WP_PSR_Logger( $setttings ); 80 | 81 | $sut->setLogger( $logger ); 82 | $context = json_decode( '{ "type": 2, "message": "Trying to access array offset on value of type bool", "file": "\/Users\/brianhenry\/Sites\/bh-wc-shipment-tracking-updates\/src\/strauss\/jamiemadden\/licenseserver\/src\/class-slswc-client.php", "line": 296, "debug_backtrace": [ { "file": "\/Users\/brianhenry\/Sites\/bh-wc-shipment-tracking-updates\/src\/strauss\/brianhenryie\/bh-wp-logger\/src\/PHP\/class-php-shutdown-handler.php", "lineNumber": 87, "arguments": [], "applicationFrame": true, "method": "handle" }, { "file": "unknown", "lineNumber": 0, "arguments": [], "applicationFrame": false, "method": "[top]", "class": null } ], "filters": [] }', true ); 83 | 84 | /** 85 | * Return null to cancel logging. 86 | * 87 | * @pararm array{level:string,message:string,context:array} $log_data 88 | * @param Logger_Settings_Interface $settings 89 | * @param BH_WP_PSR_Logger $bh_wp_psr_logger 90 | */ 91 | add_filter( 92 | 'plugin-slug_bh_wp_logger_log', 93 | function ( array $log_data, $settings, $bh_wp_psr_logger ) { 94 | return null; 95 | }, 96 | 10, 97 | 3 98 | ); 99 | 100 | $sut->log( LogLevel::ERROR, 'Trying to access array offset on value of type bool', $context ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/wpunit/class-logger-settings-trait-wpunit-Test.php: -------------------------------------------------------------------------------- 1 | get_log_level() ); 27 | } 28 | 29 | /** 30 | * @covers ::get_log_level 31 | */ 32 | public function test_get_log_level_wp_options(): void { 33 | 34 | $sut = new class() { 35 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 36 | 37 | public function get_plugin_basename(): string { 38 | return 'test-plugin/test-plugin.php'; 39 | } 40 | }; 41 | 42 | update_option( 'test-plugin_log_level', 'error' ); 43 | 44 | self::assertEquals( 'error', $sut->get_log_level() ); 45 | } 46 | 47 | /** 48 | * @covers ::get_log_level 49 | */ 50 | public function test_get_log_level_no_basename(): void { 51 | 52 | $sut = new class() { 53 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 54 | 55 | public function get_plugin_basename(): string { 56 | throw new Exception(); 57 | } 58 | }; 59 | 60 | self::assertEquals( 'none', $sut->get_log_level() ); 61 | } 62 | 63 | /** 64 | * @covers ::get_plugin_name 65 | */ 66 | public function test_get_plugin_name(): void { 67 | 68 | $sut = new class() { 69 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 70 | 71 | public function get_plugin_basename(): string { 72 | return 'test-plugin/test-plugin.php'; 73 | } 74 | }; 75 | 76 | $plugins_array = array( 77 | '' => array( 78 | 'test-plugin/test-plugin.php' => 79 | array( 80 | 'Name' => 'BH WP Logger Test Plugin', 81 | ), 82 | ), 83 | ); 84 | 85 | wp_cache_set( 'plugins', $plugins_array, 'plugins' ); 86 | 87 | self::assertEquals( 'BH WP Logger Test Plugin', $sut->get_plugin_name() ); 88 | } 89 | 90 | /** 91 | * @covers ::get_plugin_slug 92 | */ 93 | public function test_get_plugin_slug(): void { 94 | 95 | $sut = new class() { 96 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 97 | 98 | public function get_plugin_basename(): string { 99 | return 'test-plugin/test-plugin.php'; 100 | } 101 | }; 102 | 103 | self::assertEquals( 'test-plugin', $sut->get_plugin_slug() ); 104 | } 105 | 106 | /** 107 | * @covers ::get_plugin_basename 108 | */ 109 | public function test_get_plugin_slug1(): void { 110 | 111 | $this->markTestIncomplete( 'TODO' ); 112 | 113 | $plugins_array = array( 114 | 'bh-wp-logger/bh-wp-logger' => 115 | array( 116 | 'Name' => 'BH WP Logger Test Plugin', 117 | 'PluginURI' => 'http://github.com/username/bh-wp-logger-test-plugin/', 118 | 'Version' => '1.0.0', 119 | 'Description' => 'This is a short description of what the plugin does. It\'s displayed in the WordPress admin area.', 120 | 'Title' => 'BH WP Logger Test Plugin', 121 | ), 122 | 'woocommerce/woocommerce.php' => 123 | array( 124 | 'WC requires at least' => '', 125 | 'WC tested up to' => '', 126 | 'Woo' => '', 127 | 'Name' => 'WooCommerce', 128 | 'PluginURI' => 'https://woocommerce.com/', 129 | 'Version' => '7.4.1', 130 | 'Description' => 'An eCommerce toolkit that helps you sell anything. Beautifully.', 131 | 'Author' => 'Automattic', 132 | 'AuthorURI' => 'https://woocommerce.com', 133 | 'TextDomain' => 'woocommerce', 134 | 'DomainPath' => '/i18n/languages/', 135 | 'Network' => false, 136 | 'RequiresWP' => '5.9', 137 | 'RequiresPHP' => '7.2', 138 | 'UpdateURI' => '', 139 | 'Title' => 'WooCommerce', 140 | 'AuthorName' => 'Automattic', 141 | ), 142 | ); 143 | 144 | wp_cache_set( 'plugins', $plugins_array, 'plugins' ); 145 | 146 | $sut = new class() { 147 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 148 | }; 149 | 150 | global $wp_plugin_paths; 151 | $wp_plugin_paths['/Users/brianhenry/Sites/bh-wp-logger/wordpress/wp-content/plugins/bh-wp-logger'] = '/Users/brianhenry/Sites/bh-wp-logger'; 152 | 153 | $result = $sut->get_plugin_slug(); 154 | 155 | $this->assertEquals( 'bh-wp-logger', $result ); 156 | } 157 | 158 | /** 159 | * @covers ::get_plugin_basename 160 | */ 161 | public function test_get_plugin_slugssss(): void { 162 | 163 | $this->markTestIncomplete( 'TODO' ); 164 | 165 | global $wp_plugin_paths; 166 | $wp_plugin_paths['/Users/brianhenry/Sites/bh-wp-logger/wordpress/wp-content/plugins/bh-wp-logger-test-plugin'] = realpath( '/Users/brianhenry/Sites/bh-wp-logger/test-plugin' ); 167 | 168 | $test_file = '/Users/brianhenry/Sites/bh-wp-logger/wordpress/wp-content/plugins/bh-wp-logger-test-plugin/admin/class-admin.php'; 169 | 170 | $realpath_test_file = realpath( $test_file ); 171 | 172 | // Returns `bh-wp-logger-test-plugin/admin/class-admin.php`. 173 | $plugin_basename = plugin_basename( $realpath_test_file ); 174 | 175 | $plugin_slug = explode( '/', $plugin_basename )[0]; 176 | self::assertEquals( 'bh-wp-logger-test-plugin', $plugin_slug ); 177 | } 178 | 179 | 180 | /** 181 | * @covers ::get_plugin_basename 182 | */ 183 | public function test_discover_symlinked_plugin_relative_directory(): void { 184 | 185 | $this->markTestIncomplete( 'TODO' ); 186 | 187 | global $wp_plugin_paths; 188 | $wp_plugin_paths = array( 189 | '/Users/brianhenry/Sites/bh-wp-logger/wordpress/wp-content/plugins/admin-menu-editor' => '/Users/brianhenry/Sites/bh-wp-logger/wp-content/plugins/admin-menu-editor', 190 | '/Users/brianhenry/Sites/bh-wp-logger/wordpress/wp-content/plugins/bh-wp-logger' => '/Users/brianhenry/Sites/bh-wp-logger', 191 | ); 192 | 193 | $sut = new class() { 194 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 195 | }; 196 | 197 | $normal_plugin_file = '/Users/brianhenry/Sites/bh-wp-logger/subdir/file.php'; 198 | 199 | $result = $sut->discover_plugin_relative_directory( $normal_plugin_file ); 200 | 201 | $this->assertEquals( 'bh-wp-logger', $result ); 202 | } 203 | 204 | /** 205 | * @covers ::get_plugin_basename 206 | */ 207 | public function test_discover_plugin_data_simple_null(): void { 208 | 209 | $this->markTestIncomplete( 'TODO' ); 210 | 211 | $sut = new class() { 212 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 213 | }; 214 | 215 | // __DIR__ is /Users/brianhenry/Sites/bh-wp-logger/src/WP_Includes. 216 | // And $wp_plugin_paths is empty. 217 | 218 | $result = $sut->discover_plugin_data(); 219 | 220 | $this->assertNull( $result ); 221 | } 222 | 223 | /** 224 | * @covers ::get_plugin_basename 225 | */ 226 | public function test_discover_plugin_data_not_found_null(): void { 227 | 228 | $this->markTestIncomplete( 'TODO' ); 229 | 230 | global $wp_plugin_paths; 231 | $wp_plugin_paths = array( 232 | '/Users/brianhenry/Sites/bh-wp-logger/wordpress/wp-content/plugins/admin-menu-editor' => '/Users/brianhenry/Sites/bh-wp-logger/wp-content/plugins/admin-menu-editor', 233 | '/Users/brianhenry/Sites/bh-wp-logger/wordpress/wp-content/plugins/bh-wp-logger' => '/Users/brianhenry/Sites/bh-wp-logger', 234 | ); 235 | 236 | $sut = new class() { 237 | use \BrianHenryIE\WP_Logger\Logger_Settings_Trait; 238 | }; 239 | 240 | // __DIR__ is /Users/brianhenry/Sites/bh-wp-logger/src/WP_Includes. 241 | 242 | $cache_plugins = array( 243 | 'bh-wp-logger' => array(), 244 | ); 245 | 246 | wp_cache_set( 'plugins', $cache_plugins, 'plugins' ); 247 | 248 | $result = $sut->discover_plugin_data(); 249 | 250 | $this->assertNull( $result ); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /tests/wpunit/class-logger-wpunit-Test.php: -------------------------------------------------------------------------------- 1 | get_logger(); 55 | 56 | $this->assertInstanceOf( WC_PSR_Logger::class, $logger ); 57 | } 58 | 59 | /** 60 | * For a non-WooCommerce logger, Klogger should be used. 61 | * 62 | * @covers ::__construct 63 | */ 64 | public function tests_regular_logger() { 65 | 66 | $settings = new class() implements Logger_Settings_Interface { 67 | 68 | public function get_log_level(): string { 69 | return LogLevel::DEBUG; 70 | } 71 | 72 | public function get_plugin_name(): string { 73 | return 'Test'; 74 | } 75 | 76 | public function get_plugin_slug(): string { 77 | return 'test'; 78 | } 79 | 80 | public function get_plugin_basename(): string { 81 | return 'test/test.php'; 82 | } 83 | 84 | public function get_cli_base(): ?string { 85 | return null; 86 | } 87 | }; 88 | 89 | assert( ! ( $settings instanceof WooCommerce_Logger_Settings_Interface ) ); 90 | 91 | $sut = new Logger( $settings ); 92 | 93 | // We can't call wc_get_logger() until WooCommerce is loaded. 94 | do_action( 'plugins_loaded' ); 95 | 96 | $logger = $sut->get_logger(); 97 | 98 | $this->assertInstanceOf( KLogger::class, $logger ); 99 | } 100 | 101 | 102 | /** 103 | * If a plugin asks to use the WooCommerce logger, but WooCommerce is inactive, use the default KLogger. 104 | * 105 | * @covers ::__construct 106 | */ 107 | public function tests_woocommerce_inactive_logger() { 108 | 109 | $settings = new class() implements WooCommerce_Logger_Settings_Interface { 110 | 111 | public function get_log_level(): string { 112 | return LogLevel::DEBUG; 113 | } 114 | 115 | public function get_plugin_name(): string { 116 | return 'Test'; 117 | } 118 | 119 | public function get_plugin_slug(): string { 120 | return 'test'; 121 | } 122 | 123 | public function get_plugin_basename(): string { 124 | return 'test/test.php'; 125 | } 126 | 127 | public function get_cli_base(): ?string { 128 | return null; 129 | } 130 | }; 131 | 132 | // Remove WooCommerce from the active plugins list. 133 | add_filter( 134 | 'active_plugins', 135 | function ( $active_plugins ) { 136 | return array_filter( 137 | $active_plugins, 138 | function ( $element ) { 139 | return 'woocommerce/woocommerce.php' !== $element; 140 | } 141 | ); 142 | }, 143 | 999 144 | ); 145 | 146 | $sut = new Logger( $settings ); 147 | 148 | // We can't call wc_get_logger() until WooCommerce is loaded. 149 | do_action( 'plugins_loaded' ); 150 | 151 | $logger = $sut->get_logger(); 152 | 153 | $this->assertInstanceOf( KLogger::class, $logger ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/wpunit/php/class-php-error-handler-wpunit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( API::class ); 45 | 46 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 47 | $logger = new NullLogger(); 48 | 49 | $my_handler = function ( $errno, $errstr, $errfile = null, $errline = null ) {}; 50 | 51 | set_error_handler( $my_handler ); 52 | 53 | new PHP_Error_Handler( $api, $settings, $logger ); 54 | 55 | $my_handler_2 = function ( $errno, $errstr, $errfile = null, $errline = null ) {}; 56 | 57 | $previous = set_error_handler( $my_handler_2 ); 58 | 59 | $this->assertEquals( $my_handler, $previous ); 60 | } 61 | 62 | /** 63 | * Verify init does change PHP's error handler 64 | * 65 | * @covers ::init 66 | */ 67 | public function test_init() { 68 | 69 | $api = $this->makeEmpty( API::class ); 70 | 71 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 72 | $logger = new NullLogger(); 73 | 74 | $my_handler = function ( $errno, $errstr, $errfile = null, $errline = null ) {}; 75 | 76 | set_error_handler( $my_handler ); 77 | 78 | $sut = new PHP_Error_Handler( $api, $settings, $logger ); 79 | 80 | $sut->init(); 81 | 82 | $my_handler_2 = function ( $errno, $errstr, $errfile = null, $errline = null ) {}; 83 | 84 | $previous = set_error_handler( $my_handler_2 ); 85 | 86 | $this->assertNotEquals( $my_handler, $previous ); 87 | 88 | $callable_instance = $previous[0]; 89 | $callable_function = $previous[1]; 90 | 91 | $this->assertInstanceOf( PHP_Error_Handler::class, $callable_instance ); 92 | $this->assertEquals( 'plugin_error_handler', $callable_function ); 93 | } 94 | 95 | /** 96 | * Check the previous error handler is stored as an instance variable. 97 | * 98 | * @covers ::init 99 | */ 100 | public function test_stores_previous_handler() { 101 | 102 | $api = $this->makeEmpty( API::class ); 103 | 104 | $settings = $this->makeEmpty( Logger_Settings_Interface::class ); 105 | $logger = new NullLogger(); 106 | 107 | $my_handler = function ( $errno, $errstr, $errfile = null, $errline = null ) {}; 108 | 109 | set_error_handler( $my_handler ); 110 | 111 | $sut = new PHP_Error_Handler( $api, $settings, $logger ); 112 | $sut->init(); 113 | 114 | $reflector = new \ReflectionClass( $sut ); 115 | $reflector_property = $reflector->getProperty( 'previous_error_handler' ); 116 | $reflector_property->setAccessible( true ); 117 | 118 | $this->assertEquals( $my_handler, $reflector_property->getValue( $sut ) ); 119 | } 120 | 121 | /** 122 | * When the error was caused by the plugin, it should be logged with the plugin's logger. 123 | * 124 | * Tests the filename is a file from this plugin. 125 | * 126 | * @covers ::plugin_error_handler 127 | */ 128 | public function test_a_relevant_error() { 129 | 130 | $api = $this->makeEmpty( API::class ); 131 | 132 | $settings = $this->makeEmpty( 133 | Logger_Settings_Interface::class, 134 | array( 135 | 'get_plugin_slug' => 'my-plugin', 136 | 'get_plugin_basename' => 'my-plugin/my-plugin.php', 137 | ) 138 | ); 139 | 140 | $logger = $this->makeEmpty( 141 | LoggerInterface::class, 142 | array( 143 | 'warning' => \Codeception\Stub\Expected::once(), 144 | ) 145 | ); 146 | 147 | $sut = new PHP_Error_Handler( $api, $settings, $logger ); 148 | $sut->init(); 149 | 150 | $result = $sut->plugin_error_handler( 151 | E_WARNING, 152 | 'A warning message', 153 | WP_PLUGIN_DIR . '/my-plugin/a-plugin-file.php', 154 | 1 155 | ); 156 | 157 | // True means it has been handled. 158 | $this->assertTrue( $result ); 159 | } 160 | 161 | /** 162 | * When an error is caused by another plugin, it should be passed to the next error handler... or returned true|false. 163 | * 164 | * @covers ::plugin_error_handler 165 | */ 166 | public function test_irrelevant_error() { 167 | 168 | $api = $this->makeEmpty( API::class ); 169 | 170 | $settings = $this->makeEmpty( 171 | Logger_Settings_Interface::class, 172 | array( 173 | 'get_plugin_slug' => 'my-plugin', 174 | 'get_plugin_basename' => 'my-plugin/my-plugin.php', 175 | ) 176 | ); 177 | 178 | $logger = $this->makeEmpty( 179 | LoggerInterface::class, 180 | array( 181 | 'warning' => \Codeception\Stub\Expected::never(), 182 | ) 183 | ); 184 | 185 | $sut = new PHP_Error_Handler( $api, $settings, $logger ); 186 | $sut->init(); 187 | 188 | $result = $sut->plugin_error_handler( 189 | E_WARNING, 190 | 'A warning message', 191 | WP_PLUGIN_DIR . '/another-plugin/a-plugin-file.php', 192 | 1 193 | ); 194 | 195 | // True means it has been handled. 196 | $this->assertFalse( $result ); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/wpunit/wp-includes/class-functions-wpunit-Test.php: -------------------------------------------------------------------------------- 1 | makeEmpty( 34 | API_Interface::class, 35 | array( 36 | 'is_backtrace_contains_plugin' => true, 37 | ) 38 | ); 39 | $settings = $this->makeEmpty( 40 | Logger_Settings_Interface::class, 41 | array( 42 | 'get_plugin_slug' => 'test-plugin', 43 | ) 44 | ); 45 | $logger = new ColorLogger(); 46 | 47 | $sut = new Functions( $api, $settings, $logger ); 48 | 49 | assert( false === get_transient( 'log_deprecated_function_my_deprecated_function_test-plugin' ) ); 50 | assert( false === has_filter( 'deprecated_function_trigger_error', '__return_false' ) ); 51 | 52 | $sut->log_deprecated_functions_only_once_per_day( 'my_deprecated_function', 'my_replacement_function', '5.9.0' ); 53 | 54 | $log_message = 'my_deprecated_function is deprecated since version 5.9.0! Use my_replacement_function instead.'; 55 | 56 | $this->assertTrue( $logger->hasWarning( $log_message ) ); 57 | 58 | $this->assertNotFalse( get_transient( 'log_deprecated_function_my_deprecated_function_test-plugin' ) ); 59 | 60 | // `has_filter` function returns the priority. 61 | $this->assertNotFalse( has_filter( 'deprecated_function_trigger_error', '__return_false' ) ); 62 | } 63 | 64 | /** 65 | * @covers ::log_deprecated_functions_only_once_per_day() 66 | */ 67 | public function test_return_early_when_not_related_to_this_plugin(): void { 68 | 69 | /** 70 | * WP Browser adds the `deprecated_function_trigger_error` `__return_false` filter, so we need to remove it to verify behaviour. 71 | * 72 | * @see WPTestCase::expectDeprecated() 73 | */ 74 | remove_filter( 'deprecated_function_trigger_error', '__return_false' ); 75 | 76 | $api = $this->makeEmpty( 77 | API_Interface::class, 78 | array( 79 | 'is_backtrace_contains_plugin' => false, 80 | ) 81 | ); 82 | $settings = $this->makeEmpty( Logger_Settings_Interface::class, ); 83 | $logger = new ColorLogger(); 84 | 85 | $sut = new Functions( $api, $settings, $logger ); 86 | 87 | $sut->log_deprecated_functions_only_once_per_day( 'my_deprecated_function', 'my_replacement_function', '5.9.0' ); 88 | 89 | $log_message = 'my_deprecated_function is deprecated since version 5.9.0! Use my_replacement_function instead.'; 90 | 91 | $this->assertFalse( $logger->hasWarning( $log_message ) ); 92 | } 93 | 94 | /** 95 | * @covers ::log_deprecated_functions_only_once_per_day() 96 | */ 97 | public function test_do_not_log_again_if_transient_present(): void { 98 | 99 | $api = $this->makeEmpty( 100 | API_Interface::class, 101 | array( 102 | 'is_backtrace_contains_plugin' => true, 103 | ) 104 | ); 105 | $settings = $this->makeEmpty( 106 | Logger_Settings_Interface::class, 107 | array( 108 | 'get_plugin_slug' => 'test-plugin', 109 | ) 110 | ); 111 | $logger = new ColorLogger(); 112 | 113 | $sut = new Functions( $api, $settings, $logger ); 114 | 115 | assert( false === get_transient( 'log_deprecated_function_my_deprecated_function_test-plugin' ) ); 116 | 117 | $sut->log_deprecated_functions_only_once_per_day( 'my_deprecated_function', 'my_replacement_function', '5.9.0' ); 118 | 119 | remove_filter( 'deprecated_function_trigger_error', '__return_false' ); 120 | 121 | assert( false === has_filter( 'deprecated_function_trigger_error', '__return_false' ) ); 122 | assert( false !== get_transient( 'log_deprecated_function_my_deprecated_function_test-plugin' ) ); 123 | 124 | $logger = new ColorLogger(); 125 | $sut->setLogger( $logger ); 126 | 127 | $sut->log_deprecated_functions_only_once_per_day( 'my_deprecated_function', 'my_replacement_function', '5.9.0' ); 128 | 129 | $log_message = 'my_deprecated_function is deprecated since version 5.9.0! Use my_replacement_function instead.'; 130 | 131 | $this->assertFalse( $logger->hasWarning( $log_message ) ); 132 | 133 | // `has_filter` function returns the priority. 134 | $this->assertNotFalse( has_filter( 'deprecated_function_trigger_error', '__return_false' ) ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/wpunit/wp-includes/class-i18n-wpunit-Test.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace BH_WP_Logger_Test_Plugin\WP_Includes; 10 | 11 | /** 12 | * Class I18n_Test 13 | * 14 | * @coversNothing 15 | */ 16 | class I18n_WPUnit_Test extends \Codeception\TestCase\WPTestCase { 17 | 18 | /** 19 | * Checks if the filter run by WordPress in the load_plugin_textdomain() function is called. 20 | */ 21 | public function test_load_plugin_textdomain_function() { 22 | 23 | $called = false; 24 | $actual_domain = null; 25 | 26 | $filter = function ( $locale, $domain ) use ( &$called, &$actual_domain ) { 27 | 28 | $called = true; 29 | $actual_domain = $domain; 30 | 31 | return $locale; 32 | }; 33 | 34 | add_filter( 'plugin_locale', $filter, 10, 2 ); 35 | 36 | $i18n = new I18n(); 37 | 38 | $i18n->load_plugin_textdomain(); 39 | 40 | $this->assertTrue( $called, 'plugin_locale filter not called within load_plugin_textdomain() suggesting it has not been set by the plugin.' ); 41 | $this->assertEquals( 'bh-wp-logger-test-plugin', $actual_domain ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /wp-cli.yml: -------------------------------------------------------------------------------- 1 | path: wordpress 2 | --------------------------------------------------------------------------------