├── .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 |
--------------------------------------------------------------------------------
/.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 |
41 |
42 |
Log a message
43 |
44 |
$logger->info('message', 'context');
45 |
46 |
47 |
57 |
58 |
59 |
60 |
61 |
Trigger a PHP error
62 |
63 |
trigger_error( 'message', E_USER_NOTICE );
64 |
65 |
72 |
73 |
74 |
75 |
Trigger a WordPress doing_it_wrong
warning
76 |
77 |
83 |
84 |
85 |
86 |
Throw an [uncaught] Exception
87 |
88 |
throw new \Exception( 'log test exception' );
89 |
90 |
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 |
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 |
--------------------------------------------------------------------------------