├── CODEOWNERS
├── .gitignore
├── docs
├── wplc.png
└── CONTRIBUTING.md
├── features
├── extra
│ └── no-mail.php
├── load-wp-cli.feature
├── plugin.feature
├── theme.feature
├── sessions.feature
├── steps
│ ├── when.php
│ ├── given.php
│ └── then.php
├── cron.feature
├── objectcache.feature
├── bootstrap
│ ├── Process.php
│ ├── support.php
│ ├── FeatureContext.php
│ └── utils.php
├── config.feature
└── general.feature
├── php
├── pantheon
│ ├── boot.php
│ ├── views
│ │ ├── checklist.php
│ │ └── table.php
│ ├── checkinterface.php
│ ├── checkimplementation.php
│ ├── view.php
│ ├── checker.php
│ ├── filesearcher.php
│ ├── messenger.php
│ ├── checks
│ │ ├── sessions.php
│ │ ├── objectcache.php
│ │ ├── plugins.php
│ │ ├── themes.php
│ │ ├── config.php
│ │ ├── database.php
│ │ ├── cron.php
│ │ └── general.php
│ └── utils.php
└── commands
│ └── launchcheck.php
├── box.json
├── catalog-info.yaml
├── .wp_launch_check.stub
├── phpunit.xml
├── composer.json
├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── validate.yml
│ └── release.yml
├── LICENSE
├── utils
├── behat-tags.php
└── make-phar.php
├── README.md
└── CHECKS.md
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @pantheon-systems/site-experience
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | *.phar
3 | vendor/*
4 | behat/*
5 | /.idea
6 |
--------------------------------------------------------------------------------
/docs/wplc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pantheon-systems/wp_launch_check/HEAD/docs/wplc.png
--------------------------------------------------------------------------------
/features/extra/no-mail.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/php/pantheon/checkinterface.php:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/php/pantheon/checkimplementation.php:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | ./tests/
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pantheon-systems/wp_launch_check",
3 | "description": "Performs performance and security checks for WordPress.",
4 | "type": "wp-cli-package",
5 | "license": "MIT",
6 | "authors": [],
7 | "minimum-stability": "stable",
8 | "autoload": {
9 | "files": [
10 | "php/commands/launchcheck.php"
11 | ]
12 | },
13 | "require-dev": {
14 | "humbug/box": "^2",
15 | "behat/behat": "^2"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # This file is for unifying the coding style for different editors and IDEs
2 | # editorconfig.org
3 |
4 | # WordPress Coding Standards
5 | # https://make.wordpress.org/core/handbook/coding-standards/
6 |
7 | root = true
8 |
9 | [*]
10 | charset = utf-8
11 | end_of_line = lf
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 | indent_style = tab
15 |
16 | [{.jshintrc,*.json,*.yml}]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [{*.txt,wp-config-sample.php}]
21 | end_of_line = crlf
22 |
--------------------------------------------------------------------------------
/php/pantheon/view.php:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain dependencies for GitHub Actions
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "daily"
8 | open-pull-requests-limit: 10
9 | commit-message:
10 | prefix: "ci"
11 | include: "scope"
12 | labels:
13 | - "dependencies"
14 | - "github_actions"
15 | reviewers:
16 | - "dependabot"
17 |
18 | # Maintain dependencies for Composer
19 | - package-ecosystem: "composer"
20 | directory: "/"
21 | schedule:
22 | interval: "weekly"
23 | open-pull-requests-limit: 10
24 | commit-message:
25 | prefix: "composer"
26 | include: "scope"
27 | labels:
28 | - "dependencies"
29 | - "php"
30 | versioning-strategy: "increase-if-necessary"
31 | allow:
32 | - dependency-type: "direct"
33 | - dependency-type: "indirect"
34 |
--------------------------------------------------------------------------------
/features/theme.feature:
--------------------------------------------------------------------------------
1 | Feature: Test WordPress for themes with known security issues
2 |
3 | Scenario: A WordPress install with a theme with a known security issue
4 | Given a WP install
5 | And I run `wp theme install twentyfifteen --version=1.1 --force`
6 | And I run `wp theme update twentyfifteen --dry-run`
7 |
8 | When I run `wp launchcheck all --all`
9 | Then STDOUT should contain:
10 | """
11 | Found one theme needing updates
12 | """
13 | And STDOUT should contain:
14 | """
15 | Recommendation: You should update all out-of-date themes
16 | """
17 |
18 | Scenario: A WordPress install with no theme security issues
19 | Given a WP install
20 | And I run `wp theme install twentyfifteen --version=1.1 --force`
21 | And I run `wp theme update twentyfifteen`
22 |
23 | When I run `wp launchcheck all --all`
24 | Then STDOUT should contain:
25 | """
26 | Found 0 themes needing updates
27 | """
28 |
--------------------------------------------------------------------------------
/php/pantheon/checker.php:
--------------------------------------------------------------------------------
1 | callbacks[get_class($object)] = $object;
24 | }
25 |
26 | public function execute() {
27 | foreach($this->callbacks as $class => $object) {
28 | $object->init();
29 | }
30 |
31 | foreach($this->callbacks as $class => $object) {
32 | $object->run();
33 | }
34 |
35 | foreach($this->callbacks as $class => $object) {
36 | $object->message(Messenger::instance());
37 | }
38 | }
39 |
40 | public function callbacks() {
41 | return $this->callbacks;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Pantheon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/php/pantheon/filesearcher.php:
--------------------------------------------------------------------------------
1 | finder = new Finder();
17 | $this->dir = $dir;
18 | self::$instance = $this;
19 | return $this;
20 | }
21 |
22 | public function execute() {
23 | foreach($this->callbacks() as $class => $object) {
24 | $object->init();
25 | }
26 |
27 | $files = $this->finder->files()->in($this->dir)->name("*.php");
28 | foreach ( $files as $file ) {
29 | if (\WP_CLI::get_config('debug')) {
30 | \WP_CLI::line( sprintf("-> %s",$file->getRelativePathname()) );
31 | }
32 |
33 | foreach($this->callbacks() as $class => $object) {
34 | $object->run($file);
35 | }
36 | }
37 |
38 | foreach($this->callbacks() as $class => $object) {
39 | $object->message(Messenger::instance());
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/features/sessions.feature:
--------------------------------------------------------------------------------
1 | Feature: Test for the existence of the PHP Native Sessions plugin
2 |
3 | Scenario: A WordPress install without the native sessions plugin
4 | Given a WP install
5 |
6 | When I run `wp launchcheck sessions`
7 | Then STDOUT should contain:
8 | """
9 | Recommendation: You should ensure that the Native PHP Sessions plugin is installed and activated - https://wordpress.org/plugins/wp-native-php-sessions/
10 | """
11 |
12 | Scenario: A WordPress install with the native sessions plugin installed but not active
13 | Given a WP install
14 | And I run `wp plugin install wp-native-php-sessions`
15 |
16 | When I run `wp launchcheck sessions`
17 | Then STDOUT should contain:
18 | """
19 | Recommendation: You should ensure that the Native PHP Sessions plugin is installed and activated - https://wordpress.org/plugins/wp-native-php-sessions/
20 | """
21 |
22 | Scenario: A WordPress install with the native sessions plugin installed and active
23 | Given a WP install
24 | And I run `wp plugin install wp-native-php-sessions`
25 | And I run `wp plugin activate wp-native-php-sessions`
26 |
27 | When I run `wp launchcheck sessions`
28 | Then STDOUT should contain:
29 | """
30 | Recommendation: No action required
31 | """
32 |
--------------------------------------------------------------------------------
/utils/behat-tags.php:
--------------------------------------------------------------------------------
1 | ' )
38 | );
39 |
40 | # Skip Github API tests by default because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612
41 | $skip_tags[] = '@github-api';
42 |
43 | if ( !empty( $skip_tags ) ) {
44 | echo '--tags=~' . implode( '&&~', $skip_tags );
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/features/steps/when.php:
--------------------------------------------------------------------------------
1 | 'run_check',
10 | 'try' => 'run'
11 | );
12 | $method = $map[ $mode ];
13 |
14 | return $proc->$method();
15 | }
16 |
17 | $steps->When( '/^I launch in the background `([^`]+)`$/',
18 | function ( $world, $cmd ) {
19 | $world->background_proc( $cmd );
20 | }
21 | );
22 |
23 | $steps->When( '/^I (run|try) `([^`]+)`$/',
24 | function ( $world, $mode, $cmd ) {
25 | $cmd = $world->replace_variables( $cmd );
26 | $world->result = invoke_proc( $world->proc( $cmd ), $mode );
27 | }
28 | );
29 |
30 | $steps->When( "/^I (run|try) `([^`]+)` from '([^\s]+)'$/",
31 | function ( $world, $mode, $cmd, $subdir ) {
32 | $cmd = $world->replace_variables( $cmd );
33 | $world->result = invoke_proc( $world->proc( $cmd, array(), $subdir ), $mode );
34 | }
35 | );
36 |
37 | $steps->When( '/^I (run|try) the previous command again$/',
38 | function ( $world, $mode ) {
39 | if ( !isset( $world->result ) )
40 | throw new \Exception( 'No previous command.' );
41 |
42 | $proc = Process::create( $world->result->command, $world->result->cwd, $world->result->env );
43 | $world->result = invoke_proc( $proc, $mode );
44 | }
45 | );
46 |
47 |
--------------------------------------------------------------------------------
/features/cron.feature:
--------------------------------------------------------------------------------
1 | Feature: Check crons
2 |
3 | Background:
4 | Given a WP install
5 |
6 | Scenario: Cron check is healthy against a normal WordPress install
7 | When I run `wp launchcheck cron`
8 | Then STDOUT should contain:
9 | """
10 | Cron is enabled.
11 | """
12 | And STDOUT should not contain:
13 | """
14 | cron job(s) with an invalid time.
15 | """
16 | And STDOUT should not contain:
17 | """
18 | Some jobs are registered more than 10 times, which is excessive and may indicate a problem with your code.
19 | """
20 | And STDOUT should not contain:
21 | """
22 | This is too many to display and may indicate a problem with your site.
23 | """
24 |
25 | Scenario: WP Launch Check normally outputs a list of crons
26 | When I run `wp launchcheck cron`
27 | Then STDOUT should contain:
28 | """
29 | wp_version_check
30 | """
31 |
32 | Scenario: WP Launch Check warns when DISABLE_WP_CRON is defined to be true
33 | Given a local-config.php file:
34 | """
35 | No object-cache.php exists
10 | """
11 | And STDOUT should contain:
12 | """
13 | Use Object Cache Pro to speed up your backend
14 | """
15 |
16 | Scenario: An object cache is present but it's not Object Cache Pro
17 | Given a WP install
18 | And I run `wp plugin install wp-lcache --activate`
19 | And I run `wp lcache enable`
20 |
21 | When I run `wp launchcheck object-cache`
22 | Then STDOUT should contain:
23 | """
24 |
object-cache.php exists
25 | """
26 | And STDOUT should contain:
27 | """
28 | Use Object Cache Pro to speed up your backend
29 | """
30 |
31 | Scenario: WP Redis is present as the enabled object-cache
32 | Given a WP install
33 | # TODO Remove the version flag.
34 | And I run `wp plugin install wp-redis --activate`
35 | And I run `wp redis enable`
36 |
37 | When I run `wp launchcheck object-cache`
38 | Then STDOUT should contain:
39 | """
40 |
object-cache.php exists
41 | """
42 | And STDOUT should contain:
43 | """
44 | WP Redis for object caching was found. We recommend using Object Cache Pro
45 | """
46 |
--------------------------------------------------------------------------------
/utils/make-phar.php:
--------------------------------------------------------------------------------
1 | startBuffering();
30 |
31 | // PHP files
32 | $finder = new Finder();
33 | $finder
34 | ->files()
35 | ->ignoreVCS(true)
36 | ->name('*.php')
37 | ->in(WP_LAUNCH_CHECK_ROOT . '/php')
38 | ->exclude('test')
39 | ->exclude('tests')
40 | ->exclude('Tests')
41 | ;
42 |
43 | foreach ( $finder as $file ) {
44 | add_file( $phar, $file );
45 | }
46 |
47 | $phar->setStub( <<
57 | EOB
58 | );
59 |
60 | $phar->stopBuffering();
61 |
62 | echo "Generated " . DEST_PATH . "\n";
63 |
--------------------------------------------------------------------------------
/php/pantheon/messenger.php:
--------------------------------------------------------------------------------
1 | addMessage($message);
24 | }
25 |
26 | public function addMessage($message) {
27 | return array_push($this->messages, $message);
28 | }
29 |
30 | /**
31 | * Emit the message in specified format
32 | *
33 | * @params $format string optional - options are "raw","json"
34 | */
35 | public static function emit($format='raw') {
36 | $messenger = self::instance();
37 | switch($format) {
38 | case 'pantheon':
39 | case 'json':
40 | $formatted = array();
41 | foreach($messenger->messages as $message) {
42 | $formatted[$message['name']] = $message;
43 | }
44 | \WP_CLI::print_value($formatted,array('format'=>'json'));
45 | break;
46 | case 'raw':
47 | case 'default':
48 | foreach ($messenger->messages as $message) {
49 | // colorize
50 | if ( $message['score'] == 2 ) {
51 | $color = "%G";
52 | } elseif ( $message['score'] == 0 ) {
53 | $color = "%C";
54 | } else {
55 | $color = "%R";
56 | }
57 |
58 | $recommendation = isset( $message['action'] ) ? sprintf( "Recommendation: %s", $message['action'] ) : '';
59 |
60 | // @todo might be a better way to do this
61 | echo \cli\Colors::colorize( sprintf(str_repeat('-',80).PHP_EOL."%s: (%s) \n%s\nResult:%s %s\n%s\n\n".PHP_EOL,
62 | strtoupper($message['label']),
63 | $message['description'],
64 | str_repeat('-',80),
65 | $color,
66 | $message['result'].'%n', // ugly
67 | // Check for a recommended action before printing something.
68 | $recommendation )
69 | );
70 | }
71 | break;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/php/pantheon/checks/sessions.php:
--------------------------------------------------------------------------------
1 | description = 'Sessions only work when sessions plugin is enabled';
14 | $this->score = 0;
15 | $this->result = '';
16 | $this->label = 'PHP Sessions';
17 | $this->has_plugin = class_exists("Pantheon_Sessions");
18 | // If the plugin was not found, define the recommended action.
19 | // Otherwise, we don't want to recommend anything, we're all good here.
20 | $this->action = ! $this->has_plugin ? 'You should ensure that the Native PHP Sessions plugin is installed and activated - https://wordpress.org/plugins/wp-native-php-sessions/' : 'No action required';
21 |
22 | return $this;
23 | }
24 |
25 | public function run($file) {
26 | if ($this->has_plugin) return;
27 | $regex = '.*(session_start|\$_SESSION).*';
28 | preg_match('#'.$regex.'#s',$file->getContents(), $matches, PREG_OFFSET_CAPTURE );
29 | if ( $matches ) {
30 | $matches = Utils::sanitize_data($matches);
31 | $note = '';
32 | if (count($matches) > 1) {
33 | array_shift($matches);
34 | }
35 | foreach ($matches as $match) {
36 | $this->alerts[] = array( 'class' => 'error','data'=>array($file->getRelativePathname(),$match[1] + 1, substr($match[0],0,50)));
37 | }
38 | }
39 | return $this;
40 | }
41 |
42 | public function message(Messenger $messenger) {
43 | if (!empty($this->alerts)) {
44 | $checks = array( array(
45 | 'message' => sprintf( "Found %s files that reference sessions. %s ", count($this->alerts), $this->action ),
46 | 'class' => 'fail',
47 | ) );
48 | $this->result .= View::make('checklist', array('rows'=>$checks));
49 | $this->result .= View::make('table', array('headers'=>array('File','Line','Match'),'rows'=>$this->alerts));
50 | $this->score = 2;
51 | } else {
52 | if ( $this->has_plugin ) {
53 | $this->result .= 'You are running wp-native-php-sessions plugin.';
54 | } else {
55 | $this->result .= 'No files referencing sessions found.';
56 | }
57 | }
58 | $messenger->addMessage(get_object_vars($this));
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # WP Launch Check
4 |
5 | WP Launch Check is an extension for WP-CLI designed for Pantheon.io WordPress customers. While designed initially for the Pantheon dashboard it is intended to be fully usable outside of Pantheon.
6 |
7 | [](https://github.com/pantheon-systems/wp_launch_check/actions/workflows/validate.yml)
8 | [](https://github.com/pantheon-systems/wp_launch_check/actions/workflows/release.yml)
9 | [](https://pantheon.io/docs/oss-support-levels#actively-maintained-support)
10 |
11 |
12 | To use WP Launch Check simply run the ```wp launchcheck ``` command like you would any other WP-CLI command.
13 |
14 | For more information about WP-CLI you can visit [their github page](https://github.com/wp-cli/wp-cli).
15 |
16 | ## Installing
17 |
18 | Installing this package requires WP-CLI v0.23.0 or greater. Update to the latest stable release with `wp cli update`.
19 |
20 | Once you've done so, you can install this package with `wp package install pantheon-systems/wp_launch_check`.
21 |
22 | ## Available commands
23 |
24 | Below is a summary of the available commands. *Full technical description of each check run by each command can be found in the [CHECKS.md](CHECKS.md)*
25 |
26 | * **wp launchcheck cron** : Checks whether cron is enabled and what jobs are scheduled
27 | * **wp launchcheck general**: General checks for data and best practice, i.e. are you running the debug-bar plugin or have WP_DEBUG defined? This will tell you.
28 | * **wp launchcheck database**: Checks related to the databases.
29 | * **wp launchcheck object_cache**: Checks whether object caching is enabled and if on Pantheon whether redis is enabled.
30 | * **wp launchcheck sessions**: Checks for plugins referring to the php session_start() function or the superglobal ```$SESSION``` variable. In either case, if you are on a cloud/distributed platform you will need additional configuration achieve the expected functionality
31 | * **wp launchcheck plugins**: Checks plugins for updates
32 | * **wp launchcheck themes**: Checks themes for updates
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/features/bootstrap/Process.php:
--------------------------------------------------------------------------------
1 | command = $command;
19 | $proc->cwd = $cwd;
20 | $proc->env = $env;
21 |
22 | return $proc;
23 | }
24 |
25 | private $command, $cwd, $env;
26 |
27 | private function __construct() {}
28 |
29 | /**
30 | * Run the command.
31 | *
32 | * @return ProcessRun
33 | */
34 | public function run() {
35 | $cwd = $this->cwd;
36 |
37 | $descriptors = array(
38 | 0 => STDIN,
39 | 1 => array( 'pipe', 'w' ),
40 | 2 => array( 'pipe', 'w' ),
41 | );
42 |
43 | $proc = proc_open( $this->command, $descriptors, $pipes, $cwd, $this->env );
44 |
45 | $stdout = stream_get_contents( $pipes[1] );
46 | fclose( $pipes[1] );
47 |
48 | $stderr = stream_get_contents( $pipes[2] );
49 | fclose( $pipes[2] );
50 |
51 | return new ProcessRun( array(
52 | 'stdout' => $stdout,
53 | 'stderr' => $stderr,
54 | 'return_code' => proc_close( $proc ),
55 | 'command' => $this->command,
56 | 'cwd' => $cwd,
57 | 'env' => $this->env
58 | ) );
59 | }
60 |
61 | /**
62 | * Run the command, but throw an Exception on error.
63 | *
64 | * @return ProcessRun
65 | */
66 | public function run_check() {
67 | $r = $this->run();
68 |
69 | if ( $r->return_code || !empty( $r->STDERR ) ) {
70 | throw new \RuntimeException( $r );
71 | }
72 |
73 | return $r;
74 | }
75 | }
76 |
77 | /**
78 | * Results of an executed command.
79 | */
80 | class ProcessRun {
81 |
82 | /**
83 | * @var array $props Properties of executed command.
84 | */
85 | public function __construct( $props ) {
86 | foreach ( $props as $key => $value ) {
87 | $this->$key = $value;
88 | }
89 | }
90 |
91 | /**
92 | * Return properties of executed command as a string.
93 | *
94 | * @return string
95 | */
96 | public function __toString() {
97 | $out = "$ $this->command\n";
98 | $out .= "$this->stdout\n$this->stderr";
99 | $out .= "cwd: $this->cwd\n";
100 | $out .= "exit status: $this->return_code";
101 |
102 | return $out;
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/php/pantheon/checks/objectcache.php:
--------------------------------------------------------------------------------
1 | name = 'objectcache';
14 | $this->action = 'No action required';
15 | $this->description = 'Checking the object caching is on and responding.';
16 | $this->score = 0;
17 | $this->result = '';
18 | $this->label = 'Object Cache';
19 | $this->alerts = array();
20 | self::$instance = $this;
21 | return $this;
22 | }
23 |
24 | public function run() {
25 | global $redis_server;
26 | $object_cache_file = WP_CONTENT_DIR . '/object-cache.php';
27 | if (!file_exists($object_cache_file)) {
28 | $this->alerts[] = array("message"=> "No object-cache.php exists", "code" => 1);
29 | } else {
30 | $this->alerts[] = array("message"=> "object-cache.php exists", "code" => 0);
31 | }
32 | if ( defined( 'WP_REDIS_OBJECT_CACHE' )) {
33 | $this->alerts[] = array('message'=> 'WP Redis for object caching was found. We recommend using Object Cache Pro as a replacement. Learn More about how to install Object Cache Pro.', 'code' => 1);
34 | } elseif ( ! defined( 'WP_REDIS_CONFIG' ) || ! WP_REDIS_CONFIG ) {
35 | $this->alerts[] = array("message"=> 'Use Object Cache Pro to speed up your backend. Learn More', "code" => 1);
36 | } else {
37 | $this->alerts[] = array("message"=> "Object Cache Pro found", "code" => 0);
38 | }
39 |
40 | return $this;
41 | }
42 |
43 | public function message(Messenger $messenger) {
44 | if (!empty($this->alerts)) {
45 | $total = 0;
46 | $rows = array();
47 | // this is dumb and left over from the previous iterationg. @TODO move scoring to run() method
48 | foreach ($this->alerts as $alert) {
49 | $total += $alert['code'];
50 | $alert['class'] = 'ok';
51 | if (-1 === $alert['code']) {
52 | $alert['class'] = 'fail';
53 | } elseif( 2 > $alert['code']) {
54 | $alert['class'] = 'warning';
55 | }
56 | $rows[] = $alert;
57 | }
58 | $avg = $total/count($this->alerts);
59 | $this->result = View::make('checklist', array('rows'=> $rows) );
60 | $this->score = $avg;
61 | $this->action = "You should use Object Cache Pro";
62 | }
63 | $messenger->addMessage(get_object_vars($this));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | name: Behat tests
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 | - main
7 |
8 | jobs:
9 | validate:
10 | name: "Run validation test suite"
11 | runs-on: ubuntu-latest
12 |
13 | env:
14 | # GITHUB_CONTEXT: ${{ toJson(github) }}
15 | WP_CLI_BIN_DIR: /tmp/wp-cli-phar
16 | DB_NAME: pantheon
17 | DB_USER: pantheon
18 | DB_PASSWORD: pantheon
19 |
20 | services:
21 | mysql:
22 | image: mysql:5.7
23 | env:
24 | MYSQL_DATABASE: ${{ env.DB_NAME }}
25 | MYSQL_HOST: 127.0.0.1
26 | MYSQL_USER: ${{ env.DB_USER }}
27 | MYSQL_PASSWORD: ${{ env.DB_PASSWORD }}
28 | MYSQL_ROOT_PASSWORD: rootpass
29 | ports:
30 | - 3306:3306
31 | options: >-
32 | --health-cmd="mysqladmin ping"
33 | --health-interval=10s
34 | --health-timeout=5s
35 | --health-retries=5
36 |
37 | steps:
38 | - name: Checkout
39 | uses: actions/checkout@v4
40 |
41 | - name: Setup PHP
42 | uses: shivammathur/setup-php@v2
43 | with:
44 | php-version: 7.4
45 | ini-values: post_max_size=256M, max_execution_time=120
46 |
47 | - name: Get Composer Cache Directory
48 | id: composer-cache
49 | run: |
50 | echo "::set-output name=dir::$(composer config cache-files-dir)"
51 |
52 | - name: Cache Composer Downloads
53 | uses: actions/cache@v2
54 | with:
55 | path: ${{ steps.composer-cache.outputs.dir }}
56 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
57 | restore-keys: |
58 | ${{ runner.os }}-composer-
59 |
60 | - name: Cache PHP dependencies
61 | uses: actions/cache@v2
62 | with:
63 | path: vendor
64 | key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }}
65 |
66 | - name: Install composer dependencies
67 | run: |
68 | composer --no-interaction --no-progress --prefer-dist install
69 |
70 | - name: Install WP-CLI
71 | run: |
72 | # The Behat test suite will pick up the executable found in $WP_CLI_BIN_DIR
73 | mkdir -p $WP_CLI_BIN_DIR
74 | curl -s https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar > $WP_CLI_BIN_DIR/wp
75 | chmod +x $WP_CLI_BIN_DIR/wp
76 |
77 | - name: Generate Phar
78 | run: |
79 | php -dphar.readonly=0 vendor/bin/box build -v
80 |
81 | - name: Run Behat tests
82 | run: |
83 | vendor/bin/behat --ansi
84 |
--------------------------------------------------------------------------------
/php/pantheon/utils.php:
--------------------------------------------------------------------------------
1 | files()->in($dir)->name("*.php");
25 | $alerts = array();
26 |
27 | foreach ( $files as $file ) {
28 | if ( \WP_CLI::get_config('debug') ) {
29 | \WP_CLI::line( sprintf("-> %s",$file->getRelativePathname()) );
30 | }
31 |
32 | if ( preg_match('#'.$regex.'#s',$file->getContents()) !== 0 ) {
33 | $alerts[] = $file->getRelativePathname();
34 | }
35 | }
36 | return $alerts;
37 |
38 | }
39 |
40 | public static function load_fs() {
41 | if ( self::$fs ) {
42 | return self::$fs;
43 | }
44 |
45 | self::$fs = new filesystem();
46 | return self::$fs;
47 | }
48 |
49 | /**
50 | * Sanitizes data and keys recursively
51 | *
52 | * @param mixed $data Data to be sanitized
53 | * @param string|function $sanitizer_function Name of or the actual function with which to sanitize data
54 | * @return array|object|string
55 | */
56 | public static function sanitize_data($data, $sanitizer_function = 'htmlspecialchars') {
57 | if ( is_array( $data ) || is_object( $data ) ) {
58 | $sanitized_data = array_combine(
59 | array_map($sanitizer_function, array_keys((array)$data)),
60 | array_map('self::sanitize_data', array_values((array)$data))
61 | );
62 | return is_object( $data ) ? (object)$sanitized_data : $sanitized_data;
63 | } elseif ( is_integer($data) ) {
64 | return (string)$data;
65 | } elseif ( is_string( $data ) ) {
66 | if ( ! empty( $data ) ) {
67 | $sanitized_data = $sanitizer_function($data);
68 | $dom = new \DOMDocument;
69 | $dom->loadHTML( $sanitized_data );
70 | $anchors = $dom->getElementsByTagName('a');
71 |
72 | // Bail if our string does not only contain an anchor tag.
73 | if ( 0 === $anchors->length ) {
74 | return $sanitized_data;
75 | }
76 |
77 | $href = $anchors[0]->getAttribute('href');
78 | $sanitized_href = call_user_func($sanitizer_function, $href);
79 | $sanitized_link_text = call_user_func($sanitizer_function, $anchors[0]->textContent);
80 |
81 | // Rebuild anchor tags to ensure there are no injected attributes.
82 | $rebuilt_link = '' . $sanitized_link_text . '';
83 | return $rebuilt_link;
84 | }
85 | }
86 |
87 | return $sanitizer_function($data);
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/php/pantheon/checks/plugins.php:
--------------------------------------------------------------------------------
1 | check_all_plugins = $check_all_plugins;
15 | }
16 |
17 | public function init() {
18 | $this->action = 'No action required';
19 | $this->description = 'Looking for plugin info';
20 | if ( $this->check_all_plugins ) {
21 | $this->description .= ' ( active and inactive )';
22 | } else {
23 | $this->description .= ' ( active only )';
24 | }
25 | $this->score = 0;
26 | $this->result = '';
27 | $this->label = 'Plugins';
28 | $this->alerts = array();
29 | self::$instance = $this;
30 | return $this;
31 | }
32 |
33 | public function run() {
34 | if (!function_exists('get_plugins')) {
35 | require_once \WP_CLI::get_config('path') . '/wp-admin/includes/plugin.php';
36 | }
37 | $all_plugins = Utils::sanitize_data( get_plugins() );
38 | $update = Utils::sanitize_data( get_plugin_updates() );
39 | $report = array();
40 |
41 | foreach( $all_plugins as $plugin_path => $data ) {
42 | $slug = $plugin_path;
43 | if (stripos($plugin_path,'/')) {
44 | $slug = substr($plugin_path, 0, stripos($plugin_path,'/'));
45 | }
46 |
47 | $needs_update = 0;
48 | $available = '-';
49 | if (isset($update[$plugin_path])) {
50 | $needs_update = 1;
51 | $available = $update[$plugin_path]->update->new_version;
52 | }
53 |
54 | $report[ $slug ] = array(
55 | 'slug' => $slug,
56 | 'installed' => (string) $data['Version'],
57 | 'available' => (string) $available,
58 | 'needs_update' => (string) $needs_update,
59 | );
60 | }
61 | $this->alerts = $report;
62 | }
63 |
64 | public function message(Messenger $messenger) {
65 | if (empty($this->alerts)) {
66 | // Nothing to do. Return early.
67 | $this->result .= __( 'No plugins found' );
68 | $messenger->addMessage(get_object_vars($this));
69 | return;
70 | }
71 |
72 | $headers = array(
73 | 'slug'=> __( 'Plugin' ),
74 | 'installed'=> __( 'Current' ),
75 | 'available' => __( 'Available' ),
76 | 'needs_update'=> __( 'Needs Update' ),
77 | );
78 |
79 | $rows = array();
80 | $count_update = 0;
81 |
82 | foreach( $this->alerts as $alert ) {
83 | $class = 'ok';
84 | if ($alert['needs_update']) {
85 | $class = 'warning';
86 | $count_update++;
87 | }
88 |
89 | $rows[] = array('class'=>$class, 'data' => $alert);
90 | }
91 |
92 | $updates_message = $count_update === 1 ? __( 'Found one plugin needing updates' ) : sprintf( _n( 'Found %d plugin needing updates', 'Found %d plugins needing updates', $count_update ), $count_update );
93 | $result_message = $updates_message . ' ...';
94 | $rendered = PHP_EOL;
95 | $rendered .= "$result_message \n" . PHP_EOL;
96 | $rendered .= View::make('table', array('headers'=>$headers,'rows'=>$rows));
97 |
98 | $this->result .= $rendered;
99 | if ($count_update > 0) {
100 | $this->score = 1;
101 | $this->action = __( 'You should update all out-of-date plugins' );;
102 | }
103 |
104 | $messenger->addMessage(get_object_vars($this));
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Deploy and Release
2 | on:
3 | push:
4 | tags: ["v[0-9]+.[0-9]+.[0-9]+*"]
5 |
6 | jobs:
7 | validate:
8 | name: "Run validation test suite"
9 | runs-on: ubuntu-latest
10 | env:
11 | WP_CLI_BIN_DIR: /tmp/wp-cli-phar
12 | DB_NAME: pantheon
13 | DB_USER: pantheon
14 | DB_PASSWORD: pantheon
15 |
16 | services:
17 | mysql:
18 | image: mysql:5.7
19 | env:
20 | MYSQL_DATABASE: ${{ env.DB_NAME }}
21 | MYSQL_HOST: 127.0.0.1
22 | MYSQL_USER: ${{ env.DB_USER }}
23 | MYSQL_PASSWORD: ${{ env.DB_PASSWORD }}
24 | MYSQL_ROOT_PASSWORD: rootpass
25 | ports:
26 | - 3306:3306
27 | options: >-
28 | --health-cmd="mysqladmin ping"
29 | --health-interval=10s
30 | --health-timeout=5s
31 | --health-retries=5
32 |
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 |
37 | - name: Setup PHP
38 | uses: shivammathur/setup-php@v2
39 | with:
40 | php-version: 7.4
41 | ini-values: post_max_size=256M, max_execution_time=120
42 |
43 | - name: Get Composer Cache Directory
44 | id: composer-cache
45 | run: |
46 | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
47 |
48 | - name: Cache Composer Downloads
49 | uses: actions/cache@v4
50 | with:
51 | path: ${{ steps.composer-cache.outputs.dir }}
52 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
53 | restore-keys: |
54 | ${{ runner.os }}-composer-
55 |
56 | - name: Cache PHP dependencies
57 | uses: actions/cache@v4
58 | with:
59 | path: vendor
60 | key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }}
61 |
62 | - name: Install composer dependencies
63 | run: |
64 | composer --no-interaction --no-progress --prefer-dist install
65 |
66 | - name: Install WP-CLI
67 | run: |
68 | mkdir -p $WP_CLI_BIN_DIR
69 | curl -s https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar > $WP_CLI_BIN_DIR/wp
70 | chmod +x $WP_CLI_BIN_DIR/wp
71 |
72 | - name: Generate Phar
73 | run: |
74 | php -dphar.readonly=0 vendor/bin/box build -v
75 |
76 | - name: Run Behat tests
77 | run: |
78 | vendor/bin/behat --ansi
79 |
80 | - name: Archive phar
81 | uses: actions/upload-artifact@v4
82 | with:
83 | name: wp-launch-check-phar
84 | path: wp_launch_check.phar
85 | retention-days: 5
86 | if-no-files-found: error
87 |
88 | deploy-packages:
89 | name: Deploy
90 | runs-on: ubuntu-latest
91 | needs: [validate]
92 | permissions:
93 | contents: write
94 | steps:
95 | - name: Checkout
96 | uses: actions/checkout@v4
97 |
98 | - name: Download Phar
99 | uses: actions/download-artifact@v4
100 | with:
101 | name: wp-launch-check-phar
102 |
103 | - name: Generate changelog
104 | id: changelog
105 | uses: metcalfc/changelog-generator@v1.0.0
106 | with:
107 | myToken: ${{ secrets.ACCESS_TOKEN }}
108 |
109 | - name: Release
110 | uses: softprops/action-gh-release@v1
111 | with:
112 | token: ${{ secrets.ACCESS_TOKEN }}
113 | body: ${{ steps.changelog.outputs.changelog }}
114 | files: wp_launch_check.phar
115 | draft: false
116 | prerelease: false
--------------------------------------------------------------------------------
/php/pantheon/checks/themes.php:
--------------------------------------------------------------------------------
1 | check_all_themes = $check_all_themes;
16 | }
17 |
18 | public function init() {
19 | $this->action = 'No action required';
20 | $this->description = 'Looking for theme info';
21 | if ( $this->check_all_themes ) {
22 | $this->description .= ' ( active and inactive )';
23 | } else {
24 | $this->description .= ' ( active only )';
25 | }
26 | $this->score = 0;
27 | $this->result = '';
28 | $this->label = 'Themes';
29 | self::$instance = $this;
30 | return $this;
31 | }
32 |
33 | public function run() {
34 | if (!function_exists('wp_get_themes')) {
35 | require_once \WP_CLI::get_config('path') . '/wp-includes/theme.php';
36 | }
37 | $current_theme = wp_get_theme();
38 | $all_themes = Utils::sanitize_data( wp_get_themes() );
39 | $update = Utils::sanitize_data( get_theme_updates() );
40 | $report = array();
41 |
42 | foreach( $all_themes as $theme_path => $data ) {
43 | $slug = $theme_path;
44 | if (stripos($theme_path,'/')) {
45 | $slug = substr($theme_path, 0, stripos($theme_path,'/'));
46 | }
47 |
48 | // Check if we only want to check the active theme.
49 | if (!$this->check_all_themes) {
50 | // If theme list index doesn't match current theme, skip.
51 | if ($current_theme->stylesheet !== $slug) {
52 | continue;
53 | }
54 | }
55 |
56 | $data = wp_get_theme($slug);
57 | $version = $data->version;
58 | $needs_update = 0;
59 | $available = '-';
60 |
61 | if (isset($update[$theme_path])) {
62 | $needs_update = 1;
63 | $available = $update[$slug]->update["new_version"];
64 | }
65 |
66 | $report[$slug] = array(
67 | 'slug' => $slug,
68 | 'installed' => (string) $version,
69 | 'available' => (string) $available,
70 | 'needs_update' => (string) $needs_update,
71 | );
72 | }
73 | $this->alerts = $report;
74 | }
75 |
76 | public function message(Messenger $messenger) {
77 | if (empty($this->alerts)) {
78 | // Nothing to do. Return early.
79 | $this->result .= __( 'No themes found' );
80 | $messenger->addMessage(get_object_vars($this));
81 | return;
82 | }
83 |
84 | $theme_message = __( 'You should update all out-of-date themes' );
85 | $headers = array(
86 | 'slug' => __( 'Theme' ),
87 | 'installed' => __( 'Current' ),
88 | 'available' => __( 'Available' ),
89 | 'needs_update' => __( 'Needs Update' ),
90 | );
91 |
92 | $rows = array();
93 | $count_update = 0;
94 | foreach( $this->alerts as $alert ) {
95 | $class = 'ok';
96 | if ($alert['needs_update']) {
97 | $class = 'warning';
98 | $count_update++;
99 | }
100 | $rows[] = array('class'=>$class, 'data' => $alert);
101 | }
102 |
103 | $updates_message = $count_update === 1 ? __( 'Found one theme needing updates' ) : sprintf( _n( 'Found %d theme needing updates', 'Found %d themes needing updates', $count_update ), $count_update );
104 | $result_message = $updates_message . ' ...';
105 | $rendered = PHP_EOL;
106 | $rendered .= "$result_message \n" .PHP_EOL;
107 | $rendered .= View::make('table', array('headers'=>$headers,'rows'=>$rows));
108 |
109 | $this->result .= $rendered;
110 | if ($count_update > 0) {
111 | $this->score = 1;
112 | $this->action = $theme_message;
113 | }
114 |
115 | $messenger->addMessage(get_object_vars($this));
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/php/pantheon/checks/config.php:
--------------------------------------------------------------------------------
1 | name = 'config';
16 | $this->action = 'No action required';
17 | $this->description = 'Checking for a properly-configured wp-config';
18 | $this->score = 0;
19 | $this->result = '';
20 | $this->label = 'Config';
21 | $this->alerts = array();
22 | self::$instance = $this;
23 | return $this;
24 | }
25 |
26 | public function run() {
27 |
28 | // Can't be run twice, because it needs to run without WP loaded
29 | if ( $this->run_once ) {
30 | return $this;
31 | }
32 |
33 | $runner = \WP_CLI::get_runner();
34 | $wp_config = $runner->get_wp_config_code();
35 | eval( $wp_config );
36 |
37 | $this->checkWPCache();
38 | $this->checkNoServerNameWPHomeSiteUrl();
39 | $this->checkUsesEnvDBConfig();
40 |
41 | $this->run_once = true;
42 |
43 | return $this;
44 | }
45 |
46 | public function checkWPCache() {
47 | if (defined('WP_CACHE') && WP_CACHE ) {
48 | $this->alerts[] = array(
49 | 'code' => 1,
50 | 'class' => 'warning',
51 | 'message' => 'The WP_CACHE constant is set to true, and should be removed. Page cache plugins are unnecessary on Pantheon.',
52 | );
53 | } else {
54 | $this->alerts[] = array(
55 | 'code' => 0,
56 | 'class' => 'ok',
57 | 'message' => 'WP_CACHE not found or is set to false.',
58 | );
59 | }
60 | }
61 |
62 | public function checkUsesEnvDBConfig() {
63 |
64 | // Check is only applicable in the Pantheon environment
65 | if ( empty( $_ENV['PANTHEON_ENVIRONMENT'] ) ) {
66 | return;
67 | }
68 |
69 | $compared_values = array(
70 | 'DB_NAME',
71 | 'DB_USER',
72 | 'DB_PASSWORD',
73 | );
74 | $different_values = array();
75 | foreach( $compared_values as $key ) {
76 | if ( constant( $key ) != $_ENV[ $key ] ) {
77 | $different_values[] = $key;
78 | }
79 | }
80 | if ( constant( 'DB_HOST' ) != $_ENV['DB_HOST'] . ':' . $_ENV['DB_PORT'] ) {
81 | $different_values[] = 'DB_HOST';
82 | }
83 |
84 | if ( $different_values ) {
85 | $this->alerts[] = array(
86 | 'code' => 2,
87 | 'class' => 'warning',
88 | 'message' => 'Some database constants differ from their expected $_ENV values: ' . implode( ', ' , $different_values ),
89 | );
90 | $this->valid_db = false;
91 | $this->action = 'Please update your wp-config.php file to support $_ENV-based configuration values.';
92 | } else {
93 | $this->alerts[] = array(
94 | 'code' => 0,
95 | 'class' => 'ok',
96 | 'message' => implode( ', ', array_merge( $compared_values, array( 'DB_HOST' ) ) ) . ' are set to their expected $_ENV values.',
97 | );
98 | }
99 |
100 | }
101 |
102 | public function checkNoServerNameWPHomeSiteUrl() {
103 | $wp_config = \WP_CLI::get_runner()->get_wp_config_code();
104 | if ( preg_match( '#define\(.+WP_(HOME|SITEURL).+\$_SERVER.+SERVER_NAME#', $wp_config ) ) {
105 | $this->alerts[] = array(
106 | 'code' => 0,
107 | 'class' => 'warning',
108 | 'message' => "\$_SERVER['SERVER_NAME'] appears to be used to define WP_HOME or WP_SITE_URL, which will be unreliable on Pantheon.",
109 | );
110 | } else {
111 | $this->alerts[] = array(
112 | 'code' => 0,
113 | 'class' => 'ok',
114 | 'message' => "Verified that \$_SERVER['SERVER_NAME'] isn't being used to define WP_HOME or WP_SITE_URL.",
115 | );
116 | }
117 | }
118 |
119 | public function message(Messenger $messenger) {
120 | if (!empty($this->alerts)) {
121 | $total = 0;
122 | $rows = array();
123 | foreach ($this->alerts as $alert) {
124 | $total += $alert['code'];
125 | }
126 | $avg = $total/count($this->alerts);
127 | $this->result = View::make('checklist', array('rows'=> $this->alerts) );
128 | $this->score = $avg;
129 | }
130 | $messenger->addMessage(get_object_vars($this));
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/php/pantheon/checks/database.php:
--------------------------------------------------------------------------------
1 | name = 'database';
14 | $this->action = 'No action required';
15 | $this->description = 'Checking the database for issues.';
16 | $this->score = 0;
17 | $this->result = '';
18 | $this->label = 'Database';
19 | $this->alerts = array();
20 | self::$instance = $this;
21 | return $this;
22 | }
23 |
24 | public function run() {
25 | $this->countRows();
26 | $this->checkInnoDB();
27 | $this->checkTransients();
28 | return $this;
29 | }
30 |
31 | protected function getTables() {
32 | global $wpdb;
33 | if ( empty($this->tables) ) {
34 | $query = "select TABLES.TABLE_NAME, TABLES.TABLE_SCHEMA, TABLES.TABLE_ROWS, TABLES.DATA_LENGTH, TABLES.ENGINE from information_schema.TABLES where TABLES.TABLE_SCHEMA = '%s'";
35 | $tables = Utils::sanitize_data( $wpdb->get_results( $wpdb->prepare( $query, DB_NAME ) ) );
36 | foreach ( $tables as $table ) {
37 | $this->tables[$table->TABLE_NAME] = $table;
38 | }
39 | }
40 | return $this->tables;
41 | }
42 |
43 | protected function countRows() {
44 | global $wpdb;
45 | foreach ( $this->getTables() as $table ) {
46 | $this->tables[$table->TABLE_NAME] = $table;
47 | if ( $table->TABLE_NAME === $wpdb->prefix . 'options' ) {
48 | $options_table = $table;
49 | break;
50 | }
51 | }
52 | if ($options_table->TABLE_ROWS > 5000) {
53 | $this->alerts[] = array('code'=>1, 'message'=> sprintf("Found %s rows in options table which is more than recommended and can cause performance issues", $options_table->TABLE_ROWS), 'class'=>'warning');
54 | } else {
55 | $this->alerts[] = array('code'=>0, 'message'=> sprintf("Found %s rows in the options table.", $options_table->TABLE_ROWS), 'class'=>'ok');
56 | }
57 |
58 | $autoloads = $wpdb->get_results("SELECT * FROM " . $wpdb->options . " WHERE autoload = 'yes'");
59 | if ( 1000 < count($autoloads) ) {
60 | $this->alerts[] = array(
61 | 'code'=>1,
62 | 'message'=> sprintf("Found %d options being autoloaded, consider autoloading only necessary options", count($autoloads)),
63 | 'class'=> 'fail',
64 | );
65 | } else {
66 | $this->alerts[] = array(
67 | 'code'=>0,
68 | 'message'=> sprintf("Found %d options being autoloaded.", count($autoloads)),
69 | 'class'=> 'ok',
70 | );
71 | }
72 | }
73 |
74 | protected function checkInnoDb() {
75 | $not_innodb = $innodb = array();
76 | foreach ($this->getTables() as $table) {
77 | if ("InnoDB" == $table->ENGINE) {
78 | $innodb[] = $table->TABLE_NAME;
79 | } else {
80 | $not_innodb[] = $table->TABLE_NAME;
81 | }
82 | }
83 | if (!empty($not_innodb)) {
84 | $this->alerts[] = array(
85 | 'code'=> 2,
86 | 'message' => sprintf("The following tables are not InnoDB: %s. To fix, please see documentation: %s", join(', ', $not_innodb), 'https://pantheon.io/docs/articles/sites/database/myisam-to-innodb/' ),
87 | 'class' => 'fail',
88 | );
89 | } else {
90 | $this->alerts[] = array(
91 | 'code' => 0,
92 | 'message' => 'All tables using InnoDB storage engine.',
93 | 'class' => 'ok',
94 | );
95 | }
96 | }
97 |
98 | public function checkTransients() {
99 | global $wpdb;
100 | $query = "SELECT option_name,option_value from " . $wpdb->options . " where option_name LIKE '%_transient_%';";
101 | $transients = $wpdb->get_results($query);
102 | $this->alerts[] = array(
103 | 'code'=> 0,
104 | 'message' => sprintf("Found %d transients.", count($transients) ),
105 | 'class'=>'ok',
106 | );
107 | $expired = array();
108 | foreach( $transients as $transient ) {
109 | $transient->option_name;
110 | if ( preg_match( "#^_transient_timeout.*#s", $transient->option_name ) ) {
111 | if ($transient->option_value < time()) {
112 | $expired[] = str_replace('_timeout', '', $transient->option_name);
113 | }
114 | }
115 | }
116 | if ($expired) {
117 | $this->alerts[] = array(
118 | 'code' => 1,
119 | 'class' => 'warning',
120 | 'message' => sprintf( 'Found %s expired transients, consider deleting them.', count($expired) ),
121 | );
122 | }
123 |
124 | }
125 |
126 | public function message(Messenger $messenger) {
127 | if (!empty($this->alerts)) {
128 | $total = 0;
129 | $rows = array();
130 | // this is dumb and left over from the previous iterationg. @TODO move scoring to run() method
131 | foreach ($this->alerts as $alert) {
132 | $total += $alert['code'];
133 | $rows[] = $alert;
134 | }
135 | $avg = $total/count($this->alerts);
136 | $this->result = View::make('checklist', array('rows'=> $rows) );
137 | $this->score = round($avg);
138 | }
139 | $messenger->addMessage(get_object_vars($this));
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | ## 1. Make a RC Release including a `wp_launch_check.phar`
4 | * Make your changes to the `wp_launch_check` source code.
5 | * Determine the version number of the release by running `git tag --list`. The RC version will be one more than the latest tag.
6 | * Create a new branch off of the branch that your changes were made to and name it something like `wplc-v1.2.3-rc1` (based on the latest tag incremented on the patch version). This branch will just be for testing the release and should be different from the branch you are creating for the changes in the PR.
7 | * Temporarily update the logic so the `wp_launch_check.phar` is created on all branches. In `.github/workflows/validate.yml`, replace the two branch names with one line:
8 | ```
9 | - '**'
10 | ```
11 | * Create a Release Candidate tag based on your RC branch.
12 | ```
13 | git tag -a "v1.2.3-RC1" -m "Version 1.2.3-RC1"
14 | git push origin v1.2.3-RC1
15 | ```
16 | * When the tag is pushed, GitHub Actions will automatically create a Release including a `wp_launch_check.phar` file.
17 | * Monitor https://github.com/pantheon-systems/wp_launch_check/actions. Watch for your tag’s workflow to complete. If there are any failing tests, satisfy the failing test or rerun the job.
18 | * Review https://github.com/pantheon-systems/wp_launch_check/releases and ensure a `wp_launch_check.phar` file was created.
19 |
20 | ## 2. Generate a Quay tag for cos-wpcli based on the RC above
21 |
22 | To perform the next steps, you will need a local copy of the [`pantheon-systems/cos-framework-clis`](https://github.com/pantheon-systems/cos-framework-clis) repository.
23 |
24 | ```
25 | git clone git@github.com:pantheon-systems/cos-framework-clis.git
26 | ```
27 |
28 | * Checkout a new branch called `wplc-1.2.3-RC1`
29 | * In `wpcli/Dockerfile`, bump the version of WP Launch Check.
30 | * In `.circleci/config.yml`, do the following so the non-default branch will push a tag to Quay.
31 | 1. Remove the `if`/`fi` lines around `make push-wpcli-tags`
32 | 2. Remove two spaces of indentation prior to `make push-wpcli-tags`
33 | * Push the branch to GitHub. **Do not make a PR.**
34 | * Browse to the [branches page in GitHub](https://github.com/pantheon-systems/cos-framework-clis/branches) and click into your branch.
35 | * Above the listing of files in the branch, look next to the commit hash for an orange circle, green checkmark, or a red x. Click the colored icon to see the jobs running for this branch
36 | * Click Details next to the `ci/circleci: build-wpcli` job.
37 | * In the `push image to quay.io` step, look for the line that begins with `docker push` and note the tag number after `cos-wpcli:`
38 | * You can confirm this tag in Quay by browsing to https://quay.io/getpantheon/cos-wpcli
39 |
40 | ## 3. Test your code
41 |
42 | * Use [tagged deploys](https://github.com/pantheon-systems/infrastructure/blob/master/docs/tagged-deploys.md) for the `wpcli` container on an `appserver` associated with a test site in production or on your sandbox.
43 | * Test that your code `wp_launch_check` code is working as expected.
44 | * Take screenshots to be used in the PRs during the Deploying steps below, if applicable.
45 |
46 | ## 4. Cleanup
47 |
48 | * Delete the RC tag in `wp_launch_check` on your local and in GitHub:
49 | ```
50 | git tag -d v1.2.3-RC1
51 | git push origin :refs/tags/v1.2.3-RC1
52 | ```
53 | * Delete the RC branch on the `wp_launch_check` Branches page in GitHub (if you pushed the branch to the repository).
54 | * Delete the RC release on the `wp_launch_check` Releases page in GitHub.
55 | * Delete the RC branch on the `cos-framework-clis` Branches page in GitHub.
56 |
57 | # 5. Deploying
58 |
59 | ## Make a Release including a `wp_launch_check.phar`
60 |
61 | * On your local environment, switch to the branch that you will use to create your PR (make sure that any changes you temporarily made above to `.github/workflows/validate.yml` have been removed).
62 | * Open a PR, get approval, and merge your source branch into the `main` branch.
63 | * Create a Release tag based on the main branch. Use the same tag number used during testing, but omit the -RC# part.
64 | ```
65 | git checkout main
66 | git pull
67 | git tag -a "v1.2.3" -m "Version 1.2.3"
68 | git push origin v1.2.3
69 | ```
70 | * Monitor https://github.com/pantheon-systems/wp_launch_check/actions. Watch for your tag’s workflow to complete. If there are any failing tests, satisfy the failing test or rerun the job.
71 | * **Note:** Sometimes, the WP Launch Check tests will fail because the expected output in the Behat tests is different than the actual output. This is not uncommon and frequently can be resolved by running the tests again.
72 | * Review https://github.com/pantheon-systems/wp_launch_check/releases and ensure a `wp_launch_check.phar` file was created.
73 |
74 | ## Bump the version of Launch Check
75 |
76 | * In your clone of https://github.com/pantheon-systems/cos-framework-clis, checkout a new branch based off `master` called `wplc-1.2.3`. Delete your old, `-RC1` branch to avoid confusion.
77 | * In `wpcli/Dockerfile`, bump the version of `wp_launch_check` by incrementing the `wp_launch_version` `ARG` to the release that that you just pushed.
78 | * Push the branch to GitHub.
79 | * Open a PR referencing the above PR, get approval, and merge your source branch into the `master` branch.
80 |
--------------------------------------------------------------------------------
/php/pantheon/checks/cron.php:
--------------------------------------------------------------------------------
1 | name = 'cron';
15 | $this->action = 'No action required';
16 | $this->description = 'Checking whether cron is enabled and what jobs are scheduled';
17 | $this->score = 0;
18 | $this->result = '';
19 | $this->label = 'Cron';
20 | $this->alerts = array();
21 | self::$instance = $this;
22 | return $this;
23 | }
24 |
25 | public function run() {
26 | global $redis_server;
27 | $this->checkIsRegularCron();
28 | $this->checkCron();
29 | return $this;
30 | }
31 |
32 | public function checkIsRegularCron() {
33 | if ( defined("DISABLE_WP_CRON") and true == DISABLE_WP_CRON ) {
34 | $this->alerts[] = array(
35 | 'class' => 'pass',
36 | 'message' => 'WP-Cron is disabled. Pantheon is running `wp cron event run --due-now` once per hour.',
37 | 'code' => 0
38 | );
39 | } else {
40 | $this->alerts[] = array(
41 | 'class' => 'pass',
42 | 'message' => 'WP-Cron is enabled. Pantheon is also running `wp cron event run --due-now` once per hour.',
43 | 'code' => 0,
44 | );
45 | }
46 | }
47 |
48 | public function checkCron() {
49 | $total = 0;
50 | $invalid = 0;
51 | $overdue = 0;
52 | $past = 0;
53 | $now = time();
54 |
55 | $this->cron_rows = array();
56 | $cron = Utils::sanitize_data( get_option('cron') );
57 |
58 | // Count the cron jobs and alert if there are an excessive number scheduled.
59 | $job_name_counts = array();
60 | foreach ($cron as $timestamp => $crons) {
61 |
62 | // 'cron' option includes a 'version' key... ?!?!
63 | if ( 'version' === $timestamp ) {
64 | continue;
65 | }
66 |
67 | foreach ($crons as $job => $data) {
68 | $class = 'ok';
69 | $next = '';
70 | $data = array_shift($data);
71 |
72 | // If this is an invalid timestamp.
73 | if (!is_int($timestamp) || $timestamp == 0) {
74 | $invalid++;
75 | $class = "error";
76 | $next = 'INVALID';
77 | }
78 | // If the timestamp is in the past.
79 | else if ($timestamp < $now) {
80 | $past++;
81 | $class = "error";
82 | $next = date('M j, Y @ H:i:s', $timestamp) . ' (PAST DUE)';
83 | }
84 | $this->cron_rows[] = array('class' => $class, 'data' => array('jobname' => $job, 'schedule' => $data['schedule'], 'next' => $next));
85 | $total++;
86 | if ( ! isset( $job_name_counts[ $job ] ) ) {
87 | $job_name_counts[ $job ] = 0;
88 | }
89 | $job_name_counts[ $job ]++;
90 | }
91 | }
92 |
93 | if ($invalid) {
94 | $this->alerts[] = array(
95 | 'class' => 'fail',
96 | 'message' => "You have $invalid cron job(s) with an invalid time.",
97 | 'code' => 2
98 | );
99 | }
100 | if ($overdue) {
101 | $this->alerts[] = array(
102 | 'class' => 'pass',
103 | 'message' => "You have $past cron job(s) which are past due. Make sure that cron jobs are running on your site.",
104 | 'code' => 1
105 | );
106 | }
107 | $excessive_jobs = array();
108 | foreach( $job_name_counts as $job_name => $count ) {
109 | if ( $count > self::EXCESSIVE_DUPLICATE_JOBS ) {
110 | $excessive_jobs[] = $job_name;
111 | }
112 | }
113 | if ( ! empty( $excessive_jobs ) ) {
114 | $this->alerts[] = array(
115 | 'class' => 'fail',
116 | 'message' => 'Some jobs are registered more than 10 times, which is excessive and may indicate a problem with your code. These jobs include: ' . implode( ', ', $excessive_jobs ),
117 | 'code' => 1,
118 | );
119 | }
120 | if ($total > self::MAX_CRON_DISPLAY) {
121 | $this->alerts[] = array(
122 | 'class' => 'pass',
123 | 'message' => "You have $total cron jobs scheduled. This is too many to display and may indicate a problem with your site.",
124 | 'code' => 1
125 | );
126 | // Truncate the output.
127 | // @TODO: Put a note next to the output table reiterating that these are not the full results.
128 | $this->cron_rows = array_splice($this->cron_rows, 0, self::MAX_CRON_DISPLAY);
129 | }
130 | }
131 |
132 | public function message(Messenger $messenger) {
133 | if (!empty($this->alerts)) {
134 | $total = 0;
135 | $rows = array();
136 | foreach ($this->alerts as $alert) {
137 | $total += $alert['code'];
138 | $label = 'info';
139 | if (1 === $alert['code']) {
140 | $label = 'warning';
141 | } elseif( 2 >= $alert['code']) {
142 | $label = 'error';
143 | }
144 | $rows[] = array(
145 | 'message' => $alert['message'],
146 | 'class' => $label
147 | );
148 | }
149 |
150 | $avg = $total/count($this->alerts);
151 | $this->result = sprintf("%s\n%s", $this->description, View::make('checklist', array('rows' => $rows)));
152 |
153 | // format the cron table
154 | $rows = array();
155 | if ($this->cron_rows) {
156 | $headers = array(
157 | 'jobname' => 'Job',
158 | 'schedule' => 'Frequency',
159 | 'next' => 'Next Run',
160 | );
161 |
162 | $this->result .= sprintf( "
%s",View::make('table', array('rows' => $this->cron_rows, 'headers' => $headers)));
163 | $this->score = $avg;
164 | }
165 | }
166 | $messenger->addMessage(get_object_vars($this));
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/features/steps/given.php:
--------------------------------------------------------------------------------
1 | Given( '/^an empty directory$/',
10 | function ( $world ) {
11 | $world->create_run_dir();
12 | }
13 | );
14 |
15 | $steps->Given( '/^an empty cache/',
16 | function ( $world ) {
17 | $world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir();
18 | }
19 | );
20 |
21 | $steps->Given( '/^an? ([^\s]+) file:$/',
22 | function ( $world, $path, PyStringNode $content ) {
23 | $content = (string) $content . "\n";
24 | $full_path = $world->variables['RUN_DIR'] . "/$path";
25 | Process::create( \WP_CLI\Utils\esc_cmd( 'mkdir -p %s', dirname( $full_path ) ) )->run_check();
26 | file_put_contents( $full_path, $content );
27 | }
28 | );
29 |
30 | $steps->Given( '/^WP files$/',
31 | function ( $world ) {
32 | $world->download_wp();
33 | }
34 | );
35 |
36 | $steps->Given( '/^wp-config\.php$/',
37 | function ( $world ) {
38 | $world->create_config();
39 | }
40 | );
41 |
42 | $steps->Given( '/^a database$/',
43 | function ( $world ) {
44 | $world->create_db();
45 | }
46 | );
47 |
48 | $steps->Given( '/^a WP install$/',
49 | function ( $world ) {
50 | $world->install_wp();
51 | }
52 | );
53 |
54 | $steps->Given( "/^a WP install in '([^\s]+)'$/",
55 | function ( $world, $subdir ) {
56 | $world->install_wp( $subdir );
57 | }
58 | );
59 |
60 | $steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?install$/',
61 | function ( $world, $type = 'subdirectory' ) {
62 | $world->install_wp();
63 | $subdomains = ! empty( $type ) && 'subdomain' === $type ? 1 : 0;
64 | $world->proc( 'wp core install-network', array( 'title' => 'WP CLI Network', 'subdomains' => $subdomains ) )->run_check();
65 | }
66 | );
67 |
68 | $steps->Given( '/^these installed and active plugins:$/',
69 | function( $world, $stream ) {
70 | $plugins = implode( ' ', array_map( 'trim', explode( PHP_EOL, (string)$stream ) ) );
71 | $world->proc( "wp plugin install $plugins --activate" )->run_check();
72 | }
73 | );
74 |
75 | $steps->Given( '/^a custom wp-content directory$/',
76 | function ( $world ) {
77 | $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php";
78 |
79 | $wp_config_code = file_get_contents( $wp_config_path );
80 |
81 | $world->move_files( 'wp-content', 'my-content' );
82 | $world->add_line_to_wp_config( $wp_config_code,
83 | "define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/my-content' );" );
84 |
85 | $world->move_files( 'my-content/plugins', 'my-plugins' );
86 | $world->add_line_to_wp_config( $wp_config_code,
87 | "define( 'WP_PLUGIN_DIR', __DIR__ . '/my-plugins' );" );
88 |
89 | file_put_contents( $wp_config_path, $wp_config_code );
90 | }
91 | );
92 |
93 | $steps->Given( '/^download:$/',
94 | function ( $world, TableNode $table ) {
95 | foreach ( $table->getHash() as $row ) {
96 | $path = $world->replace_variables( $row['path'] );
97 | if ( file_exists( $path ) ) {
98 | // assume it's the same file and skip re-download
99 | continue;
100 | }
101 |
102 | Process::create( \WP_CLI\Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check();
103 | }
104 | }
105 | );
106 |
107 | $steps->Given( '/^save (STDOUT|STDERR) ([\'].+[^\'])?as \{(\w+)\}$/',
108 | function ( $world, $stream, $output_filter, $key ) {
109 |
110 | $stream = strtolower( $stream );
111 |
112 | if ( $output_filter ) {
113 | $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/';
114 | if ( false !== preg_match( $output_filter, $world->result->$stream, $matches ) )
115 | $output = array_pop( $matches );
116 | else
117 | $output = '';
118 | } else {
119 | $output = $world->result->$stream;
120 | }
121 | $world->variables[ $key ] = trim( $output, "\n" );
122 | }
123 | );
124 |
125 | $steps->Given( '/^a new Phar(?: with version "([^"]+)")$/',
126 | function ( $world, $version ) {
127 | $world->build_phar( $version );
128 | }
129 | );
130 |
131 | $steps->Given( '/^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/',
132 | function ( $world, $filepath, $output_filter, $key ) {
133 | $full_file = file_get_contents( $world->replace_variables( $filepath ) );
134 |
135 | if ( $output_filter ) {
136 | $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/';
137 | if ( false !== preg_match( $output_filter, $full_file, $matches ) )
138 | $output = array_pop( $matches );
139 | else
140 | $output = '';
141 | } else {
142 | $output = $full_file;
143 | }
144 | $world->variables[ $key ] = trim( $output, "\n" );
145 | }
146 | );
147 |
148 | $steps->Given('/^a misconfigured WP_CONTENT_DIR constant directory$/',
149 | function($world) {
150 | $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php";
151 |
152 | $wp_config_code = file_get_contents( $wp_config_path );
153 |
154 | $world->add_line_to_wp_config( $wp_config_code,
155 | "define( 'WP_CONTENT_DIR', '' );" );
156 |
157 | file_put_contents( $wp_config_path, $wp_config_code );
158 | }
159 | );
160 |
161 | $steps->Given('/^the current WP version is not the latest$/', function ($world) {
162 | // Use wp-cli to get the currently installed WordPress version.
163 | $currentVersion = $world->proc('wp core version')->run();
164 |
165 | // Normalize versions (remove new lines).
166 | $currentVersion = trim($currentVersion->stdout);
167 | $latestVersion = get_wp_version();
168 |
169 | // If there's no update available or the current version is the latest, throw an exception to skip the test.
170 | if (empty($latestVersion) || $currentVersion === $latestVersion) {
171 | $world->isLatestWPVersion = true;
172 | return;
173 | }
174 |
175 | $world->isLatestWPVersion = false;
176 | });
177 |
--------------------------------------------------------------------------------
/features/bootstrap/support.php:
--------------------------------------------------------------------------------
1 | $value ) {
71 | if ( ! compareContents( $value, $actual->$name ) )
72 | return false;
73 | }
74 | } else if ( is_array( $expected ) ) {
75 | foreach ( $expected as $key => $value ) {
76 | if ( ! compareContents( $value, $actual[$key] ) )
77 | return false;
78 | }
79 | } else {
80 | return $expected === $actual;
81 | }
82 |
83 | return true;
84 | }
85 |
86 | /**
87 | * Compare two strings containing JSON to ensure that @a $actualJson contains at
88 | * least what the JSON string @a $expectedJson contains.
89 | *
90 | * @return whether or not @a $actualJson contains @a $expectedJson
91 | * @retval true @a $actualJson contains @a $expectedJson
92 | * @retval false @a $actualJson does not contain @a $expectedJson
93 | *
94 | * @param[in] $actualJson the JSON string to be tested
95 | * @param[in] $expectedJson the expected JSON string
96 | *
97 | * Examples:
98 | * expected: {'a':1,'array':[1,3,5]}
99 | *
100 | * 1 )
101 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]}
102 | * return: true
103 | *
104 | * 2 )
105 | * actual: {'b':2,'c':3,'array':[1,2,3,4,5]}
106 | * return: false
107 | * element 'a' is missing from the root object
108 | *
109 | * 3 )
110 | * actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]}
111 | * return: false
112 | * the value of element 'a' is not 1
113 | *
114 | * 4 )
115 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]}
116 | * return: false
117 | * the contents of 'array' does not include 3
118 | */
119 | function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) {
120 | $actualValue = json_decode( $actualJson );
121 | $expectedValue = json_decode( $expectedJson );
122 |
123 | if ( !$actualValue ) {
124 | return false;
125 | }
126 |
127 | return compareContents( $expectedValue, $actualValue );
128 | }
129 |
130 | /**
131 | * Compare two strings to confirm $actualCSV contains $expectedCSV
132 | * Both strings are expected to have headers for their CSVs.
133 | * $actualCSV must match all data rows in $expectedCSV
134 | *
135 | * @param string A CSV string
136 | * @param array A nested array of values
137 | * @return bool Whether $actualCSV contains $expectedCSV
138 | */
139 | function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) {
140 | $actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) );
141 |
142 | if ( empty( $actualCSV ) )
143 | return false;
144 |
145 | // Each sample must have headers
146 | $actualHeaders = array_values( array_shift( $actualCSV ) );
147 | $expectedHeaders = array_values( array_shift( $expectedCSV ) );
148 |
149 | // Each expectedCSV must exist somewhere in actualCSV in the proper column
150 | $expectedResult = 0;
151 | foreach ( $expectedCSV as $expected_row ) {
152 | $expected_row = array_combine( $expectedHeaders, $expected_row );
153 | foreach ( $actualCSV as $actual_row ) {
154 |
155 | if ( count( $actualHeaders ) != count( $actual_row ) )
156 | continue;
157 |
158 | $actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row );
159 | if ( $actual_row == $expected_row )
160 | $expectedResult++;
161 | }
162 | }
163 |
164 | return $expectedResult >= count( $expectedCSV );
165 | }
166 |
167 | /**
168 | * Compare two strings containing YAML to ensure that @a $actualYaml contains at
169 | * least what the YAML string @a $expectedYaml contains.
170 | *
171 | * @return whether or not @a $actualYaml contains @a $expectedJson
172 | * @retval true @a $actualYaml contains @a $expectedJson
173 | * @retval false @a $actualYaml does not contain @a $expectedJson
174 | *
175 | * @param[in] $actualYaml the YAML string to be tested
176 | * @param[in] $expectedYaml the expected YAML string
177 | */
178 | function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) {
179 | $actualValue = spyc_load( $actualYaml );
180 | $expectedValue = spyc_load( $expectedYaml );
181 |
182 | if ( !$actualValue ) {
183 | return false;
184 | }
185 |
186 | return compareContents( $expectedValue, $actualValue );
187 | }
188 |
189 |
--------------------------------------------------------------------------------
/features/steps/then.php:
--------------------------------------------------------------------------------
1 | Then( '/^the return code should be (\d+)$/',
7 | function ( $world, $return_code ) {
8 | if ( $return_code != $world->result->return_code ) {
9 | throw new RuntimeException( $world->result );
10 | }
11 | }
12 | );
13 |
14 | $steps->Then( '/^(STDOUT|STDERR) should (be|contain|not contain):$/',
15 | function ( $world, $stream, $action, PyStringNode $expected ) {
16 | // Conditional handling of WP version check.
17 | if (isset($world->isLatestWPVersion) && $world->isLatestWPVersion) {
18 | return;
19 | }
20 |
21 | $stream = strtolower( $stream );
22 |
23 | $expected = $world->replace_variables( (string) $expected );
24 |
25 | checkString( $world->result->$stream, $expected, $action, $world->result );
26 | }
27 | );
28 |
29 | $steps->Then( '/^(STDOUT|STDERR) should be a number$/',
30 | function ( $world, $stream ) {
31 |
32 | $stream = strtolower( $stream );
33 |
34 | assertNumeric( trim( $world->result->$stream, "\n" ) );
35 | }
36 | );
37 |
38 | $steps->Then( '/^(STDOUT|STDERR) should not be a number$/',
39 | function ( $world, $stream ) {
40 |
41 | $stream = strtolower( $stream );
42 |
43 | assertNotNumeric( trim( $world->result->$stream, "\n" ) );
44 | }
45 | );
46 |
47 | $steps->Then( '/^STDOUT should be a table containing rows:$/',
48 | function ( $world, TableNode $expected ) {
49 | $output = $world->result->stdout;
50 | $actual_rows = explode( "\n", rtrim( $output, "\n" ) );
51 |
52 | $expected_rows = array();
53 | foreach ( $expected->getRows() as $row ) {
54 | $expected_rows[] = $world->replace_variables( implode( "\t", $row ) );
55 | }
56 |
57 | compareTables( $expected_rows, $actual_rows, $output );
58 | }
59 | );
60 |
61 | $steps->Then( '/^STDOUT should end with a table containing rows:$/',
62 | function ( $world, TableNode $expected ) {
63 | $output = $world->result->stdout;
64 | $actual_rows = explode( "\n", rtrim( $output, "\n" ) );
65 |
66 | $expected_rows = array();
67 | foreach ( $expected->getRows() as $row ) {
68 | $expected_rows[] = $world->replace_variables( implode( "\t", $row ) );
69 | }
70 |
71 | $start = array_search( $expected_rows[0], $actual_rows );
72 |
73 | if ( false === $start )
74 | throw new \Exception( $world->result );
75 |
76 | compareTables( $expected_rows, array_slice( $actual_rows, $start ), $output );
77 | }
78 | );
79 |
80 | $steps->Then( '/^STDOUT should be JSON containing:$/',
81 | function ( $world, PyStringNode $expected ) {
82 | $output = $world->result->stdout;
83 | $expected = $world->replace_variables( (string) $expected );
84 |
85 | if ( !checkThatJsonStringContainsJsonString( $output, $expected ) ) {
86 | throw new \Exception( $world->result );
87 | }
88 | });
89 |
90 | $steps->Then( '/^STDOUT should be a JSON array containing:$/',
91 | function ( $world, PyStringNode $expected ) {
92 | $output = $world->result->stdout;
93 | $expected = $world->replace_variables( (string) $expected );
94 |
95 | $actualValues = json_decode( $output );
96 | $expectedValues = json_decode( $expected );
97 |
98 | $missing = array_diff( $expectedValues, $actualValues );
99 | if ( !empty( $missing ) ) {
100 | throw new \Exception( $world->result );
101 | }
102 | });
103 |
104 | $steps->Then( '/^STDOUT should be CSV containing:$/',
105 | function ( $world, TableNode $expected ) {
106 | $output = $world->result->stdout;
107 |
108 | $expected_rows = $expected->getRows();
109 | foreach ( $expected as &$row ) {
110 | foreach ( $row as &$value ) {
111 | $value = $world->replace_variables( $value );
112 | }
113 | }
114 |
115 | if ( ! checkThatCsvStringContainsValues( $output, $expected_rows ) )
116 | throw new \Exception( $world->result );
117 | }
118 | );
119 |
120 | $steps->Then( '/^STDOUT should be YAML containing:$/',
121 | function ( $world, PyStringNode $expected ) {
122 | $output = $world->result->stdout;
123 | $expected = $world->replace_variables( (string) $expected );
124 |
125 | if ( !checkThatYamlStringContainsYamlString( $output, $expected ) ) {
126 | throw new \Exception( $world->result );
127 | }
128 | });
129 |
130 | $steps->Then( '/^(STDOUT|STDERR) should be empty$/',
131 | function ( $world, $stream ) {
132 |
133 | $stream = strtolower( $stream );
134 |
135 | if ( !empty( $world->result->$stream ) ) {
136 | throw new \Exception( $world->result );
137 | }
138 | }
139 | );
140 |
141 | $steps->Then( '/^(STDOUT|STDERR) should not be empty$/',
142 | function ( $world, $stream ) {
143 |
144 | $stream = strtolower( $stream );
145 |
146 | if ( '' === rtrim( $world->result->$stream, "\n" ) ) {
147 | throw new Exception( $world->result );
148 | }
149 | }
150 | );
151 |
152 | $steps->Then( '/^the (.+) (file|directory) should (exist|not exist|be:|contain:|not contain:)$/',
153 | function ( $world, $path, $type, $action, $expected = null ) {
154 | $path = $world->replace_variables( $path );
155 |
156 | // If it's a relative path, make it relative to the current test dir
157 | if ( '/' !== $path[0] )
158 | $path = $world->variables['RUN_DIR'] . "/$path";
159 |
160 | if ( 'file' == $type ) {
161 | $test = 'file_exists';
162 | } else if ( 'directory' == $type ) {
163 | $test = 'is_dir';
164 | }
165 |
166 | switch ( $action ) {
167 | case 'exist':
168 | if ( ! $test( $path ) ) {
169 | throw new Exception( $world->result );
170 | }
171 | break;
172 | case 'not exist':
173 | if ( $test( $path ) ) {
174 | throw new Exception( $world->result );
175 | }
176 | break;
177 | default:
178 | if ( ! $test( $path ) ) {
179 | throw new Exception( "$path doesn't exist." );
180 | }
181 | $action = substr( $action, 0, -1 );
182 | $expected = $world->replace_variables( (string) $expected );
183 | if ( 'file' == $type ) {
184 | $contents = file_get_contents( $path );
185 | } else if ( 'directory' == $type ) {
186 | $files = glob( rtrim( $path, '/' ) . '/*' );
187 | foreach( $files as &$file ) {
188 | $file = str_replace( $path . '/', '', $file );
189 | }
190 | $contents = implode( PHP_EOL, $files );
191 | }
192 | checkString( $contents, $expected, $action );
193 | }
194 | }
195 | );
196 |
--------------------------------------------------------------------------------
/CHECKS.md:
--------------------------------------------------------------------------------
1 | # WP Launch Checks Logic
2 |
3 | All the checks in this extension should be explained in detail here. This file should be organized by command and type checker
4 |
5 | There are currently two broad types of checkers.
6 | * [\Pantheon\Checker](php/pantheon/checker.php): These checks simply examine a piece of data and register and alert if the data exists. For instance, does the ```wp-content/object-cache.php``` exist? If so some object caching is enabled.
7 | * [\Pantheon\Filesearcher](php/pantheon/filesearcher.php): These checks are functionally the same as the above except that before being run the class uses [\Symfony\Component\Finder\Finder](http://symfony.com/doc/current/components/finder.html) to load a list of files to be checked and then runs the specified check on each file. The logic is slightly different here to allow the Finder operation to *only* run once even when multiple "Filesearcher" children are running
8 |
9 |
10 | The Checker oject has two key methods
11 | * ```register( Check $check )```: receives an instance of a check to run.
12 | * ```execute()```: executes all registered checks
13 |
14 | The checks themselves are all extensions of the [\Patheon\Checkimplementation](php/pantheon/Checkimplemtation.php) class, each containing the following methods:
15 | * ```init()```
16 | * ```run()```
17 | * ```message()```;
18 |
19 | The Checker object holds a collection of Check objects which it iterates and invokes each of these methods. In the case of the Filesearcher object, the ```init()``` method generates the file list ( if not already present ) and the ```run()``` method is passed a $file parameter.
20 |
21 | The message method receives a [\Pantheon\Messsenger](php/pantheon/messenger.php) and updates the various Check object properties for output. The output of each check is simply the formatted representation of the object properties.
22 |
23 | **Check Object Properties:**
24 | * ```$name```: machine name of the check for use at the index of the returned JSON ( if json is specified )
25 | * ```$description```: textual description of what the check does
26 | * ```$label```: display version of check name used on dashboard
27 | * ```$score```: used to toggle display mechanisms in the dashboard
28 | 0: ok (green)
29 | 1: warning (orange)
30 | 2: error (red)
31 | * ```$result```: rendered html returned for use on the dashboard ( @TODO this should eventually return raw output as well when dashboard is not the intended client )
32 | * ```$alerts```: an array of alerts to rendered for the ```$result```. Each alert should be an array: ``` array(
33 | 'code' => 2,
34 | 'class' => 'error',
35 | 'message' => 'This is a sample error message',
36 | );```
37 |
38 | ## Filesearchers
39 |
40 | ### Sessions
41 | **Check:** \Pantheon\Checks\Sessions;
42 | This check does a ```preg_match``` on each file passed to the run() method for the regex ```.*(session_start|SESSION).*```
43 |
44 | ## Regular Checkers
45 |
46 | ### General
47 | **Check:** [\Pantheon\Checks\General](php/pantheon/checks/general.php)
48 | This check does the following:
49 | * Checks for WP_DEBUG=True, returns 'ok' if in dev, 'warning; in live
50 | * Checks whether the debug-bar plugin is active, 'ok' in dev, 'warning' in live
51 | * Counts active plugins. Alerts if more than 100 are active
52 | * Checks database settings for ```home``` and ```siteurl``` and whether they match. If they do not it recommends fixing. You can do this with ```WP_CLI/Terminus using 'terminus wp search-replace 'domain1' 'domain2' --site=sitename --env=dev'```
53 | * Checks whether WP Super Cache and/or W3 Total Cache are found and alerts 'warning' if so.
54 |
55 | ### Database
56 | **Database:** [\Pantheon\Checks\Database](php/pantheon/checks/database.php)
57 | This check runs the following db checks
58 | * Runs this query ```SELECT TABLES.TABLE_NAME, TABLES.TABLE_SCHEMA, TABLES.TABLE_ROWS, TABLES.DATA_LENGTH, TABLES.ENGINE from information_schema.TABLES where TABLES.TABLE_SCHEMA = '%s'``` and checks that all tables as set to InnoDb storage engine, alerts 'error' if not and specifies a query that can be run to fix the issue.
59 | * Also checks number of rows in the options table. If over 10,000 it alerts 'error' because this is an indication that expired transients are stacking up or that they are using a lugin that over uses the options table. A bloated options table can be a major cause of WP performance issues.
60 | * Counts options that are set to 'autoload', alerts is more than 1,000 are found. This is relevant because WordPress runs ```SELECT * FROM wp_options WHERE autoload = 'yes'``` on every page load to prepopulate the runtime cache. In cases where the query takes to long or returns too much data this can slow down page load. The only benefit to the runtime cache comes when object caching is not in use, but it is strongly encourage that some kind of object cache is always in use.
61 | * Looks for transients and expired transients. Some plugins will use transients regularly but not add a garbage collection cron task. Core WordPress has not garbage collection for the transient api. Over time this can cause transients to bloat the ```wp_options``` database as mentioned above.
62 |
63 | ### Cron
64 | **Cron:** [\Pantheon\Checks\Cron](php/commands/checks/cron.php)
65 | This check simple examines whether ```DISABLE_WP_CRON``` evaluates ```true``` to see if cron has been disabled. ( We should probably also curl the ```wp-cron.php?doing_wp_cron``` and ensure we get a 200 ). Some hosts disable the default WP_Cron functionality, substituting a system cron, because the HTTP base WP_Cron can sometimes have race conditions develop causing what might be referred to as "runaway cron", in which HTTP multiple requests trigger the cron a small amount of time causing a spike in PHP/MySQL resource consumption. This check also dumps the scheduled tasks into a table using ```get_option('cron')```.
66 |
67 | ### object-cache
68 | **objectcache** [\Pantheon\Checks\Cron](php/commands/checks/objectcache.php)
69 | Checks is the ```wp-content/object-cache.php``` exists to determine whether object caching is in use. Checks that the ```global $redis_server``` variable is not empty to determine whether redis is being used.
70 |
71 | ### Plugins
72 | **plugins** [\Pantheon\Checks\Plugins](php/commands/checks/plugins.php)
73 | Checks for available updates and alerts 'warning' if plugins needing an update are found.
74 |
75 | ### Themes
76 | **themes** [\Pantheon\Checks\Themes](php/commands/checks/themes.php)
77 | Checks for available updates and alerts 'warning' if themes needing an update are found.
78 |
--------------------------------------------------------------------------------
/php/commands/launchcheck.php:
--------------------------------------------------------------------------------
1 | register( $config_check );
22 | $checker->execute();
23 |
24 | if ( ! $config_check->valid_db ) {
25 | WP_CLI::warning( 'Detected invalid database credentials, skipping remaining checks' );
26 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
27 | \Pantheon\Messenger::emit($format);
28 | return;
29 | }
30 |
31 | // wp-config is going to be loaded again, and we need to avoid notices
32 | @WP_CLI::get_runner()->load_wordpress();
33 | WP_CLI::add_hook( 'before_run_command', [ $this, 'maybe_switch_to_blog' ] );
34 |
35 | // WordPress is now loaded, so other checks can run
36 | $searcher = new \Pantheon\Filesearcher( WP_CONTENT_DIR );
37 | $searcher->register( new \Pantheon\Checks\Sessions() );
38 | $searcher->execute();
39 | $checker->register( new \Pantheon\Checks\Plugins(TRUE));
40 | $checker->register( new \Pantheon\Checks\Themes(TRUE));
41 | $checker->register( new \Pantheon\Checks\Cron() );
42 | $checker->register( new \Pantheon\Checks\Objectcache() );
43 | $checker->register( new \Pantheon\Checks\Database() );
44 | $checker->execute();
45 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
46 | \Pantheon\Messenger::emit($format);
47 | }
48 |
49 | /**
50 | * Switch to BLOG_ID_CURRENT_SITE if we're on a multisite.
51 | *
52 | * This forces the launchcheck command to use the main site's info for all
53 | * the checks.
54 | */
55 | public function maybe_switch_to_blog() {
56 | // Check for multisite. If we're on multisite, switch to the main site.
57 | if ( is_multisite() ) {
58 | if ( defined( 'BLOG_ID_CURRENT_SITE' ) ) {
59 | switch_to_blog( BLOG_ID_CURRENT_SITE );
60 | } else {
61 | switch_to_blog( 1 );
62 | }
63 | \WP_CLI::log( sprintf( esc_html__( 'Multisite detected. Running checks on %s site.' ), get_bloginfo( 'name' ) ) );
64 | }
65 | }
66 |
67 | /**
68 | * Checks for a properly-configured wp-config
69 | *
70 | * ## OPTIONS
71 | *
72 | * [--format=]
73 | * : use to output json
74 | *
75 | * @when before_wp_load
76 | */
77 | function config($args, $assoc_args) {
78 | $checker = new \Pantheon\Checker();
79 | $checker->register( new \Pantheon\Checks\Config() );
80 | $checker->execute();
81 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
82 | \Pantheon\Messenger::emit($format);
83 | }
84 |
85 | /**
86 | * Checks the cron
87 | *
88 | * ## OPTIONS
89 | *
90 | * [--format=]
91 | * : use to output json
92 | *
93 | */
94 | function cron($args, $assoc_args) {
95 | $checker = new \Pantheon\Checker();
96 | $checker->register( new \Pantheon\Checks\Cron() );
97 | $checker->execute();
98 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
99 | \Pantheon\Messenger::emit($format);
100 | }
101 |
102 | /**
103 | * Check database for potential issues
104 | *
105 | * ## OPTIONS
106 | *
107 | * [--format=]
108 | * : use to output json
109 | *
110 | */
111 | function database($args, $assoc_args) {
112 | $checker = new \Pantheon\Checker();
113 | $checker->register( new \Pantheon\Checks\Database() );
114 | $checker->execute();
115 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
116 | \Pantheon\Messenger::emit($format);
117 | }
118 |
119 | /**
120 | * Checks for best practice
121 | *
122 | * ## OPTIONS
123 | *
124 | * [--format=]
125 | * : use to output json
126 | *
127 | */
128 | function general($args, $assoc_args) {
129 | $checker = new \Pantheon\Checker();
130 | $checker->register( new \Pantheon\Checks\General() );
131 | $checker->execute();
132 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
133 | \Pantheon\Messenger::emit($format);
134 | }
135 |
136 | /**
137 | * checks for object caching
138 | *
139 | * ## OPTIONS
140 | *
141 | * [--format=]
142 | * : output as json
143 | *
144 | * ## EXAMPLES
145 | *
146 | * wp launchcheck object-cache
147 | *
148 | * @alias object-cache
149 | */
150 | public function object_cache($args, $assoc_args) {
151 | $checker = new \Pantheon\Checker();
152 | $checker->register( new \Pantheon\Checks\Objectcache() );
153 | $checker->execute();
154 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
155 | \Pantheon\Messenger::emit($format);
156 | }
157 |
158 | /**
159 | * Checks plugins for available updates
160 | *
161 | * ## OPTIONS
162 | *
163 | * [--all]
164 | * : check both active and inactive plugins ( default is active only )
165 | *
166 | * [--format=]
167 | * : output as json
168 | *
169 | * ## EXAMPLES
170 | *
171 | * wp launchcheck plugins --all
172 | *
173 | */
174 | public function plugins($args, $assoc_args) {
175 | $checker = new \Pantheon\Checker();
176 | $checker->register( new \Pantheon\Checks\Plugins( isset($assoc_args['all'])) );
177 | $checker->execute();
178 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
179 | \Pantheon\Messenger::emit($format);
180 | }
181 |
182 | /**
183 | * Checks themes for available updates
184 | *
185 | * ## OPTIONS
186 | *
187 | * [--all]
188 | * : check both active and inactive themes ( default is active only )
189 | *
190 | * [--format=]
191 | * : output as json
192 | *
193 | * ## EXAMPLES
194 | *
195 | * wp launchcheck themes --all
196 | *
197 | */
198 | public function themes($args, $assoc_args) {
199 | $checker = new \Pantheon\Checker();
200 | $checker->register( new \Pantheon\Checks\Themes( isset($assoc_args['all']) ) );
201 | $checker->execute();
202 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
203 | \Pantheon\Messenger::emit($format);
204 | }
205 |
206 | /**
207 | * checks the files for session_start()
208 | *
209 | * ## OPTIONS
210 | *
211 | * [--format=]
212 | * : output as json
213 | *
214 | * ## EXAMPLES
215 | *
216 | * wp launchcheck sessions
217 | *
218 | */
219 | public function sessions( $args, $assoc_args ) {
220 | $searcher = new \Pantheon\Filesearcher( WP_CONTENT_DIR );
221 | $searcher->register( new \Pantheon\Checks\Sessions() );
222 | $searcher->execute();
223 | $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'raw';
224 | \Pantheon\Messenger::emit($format);
225 | }
226 | }
227 |
228 | // register our autoloader
229 | spl_autoload_register(function($class) {
230 | if (class_exists($class)) return $class;
231 | $class = strtolower($class);
232 | if (strstr($class,"pantheon")) {
233 | $class = str_replace('\\','/',$class);
234 | $path = dirname( dirname( __FILE__ ) ) ."/".$class.'.php';
235 | if (file_exists($path)) {
236 | require_once($path);
237 | }
238 | }
239 | });
240 |
241 | if ( class_exists( 'WP_CLI' ) ) {
242 | WP_CLI::add_command( 'launchcheck', 'LaunchCheck' );
243 | }
244 |
--------------------------------------------------------------------------------
/php/pantheon/checks/general.php:
--------------------------------------------------------------------------------
1 | name = 'general';
14 | $this->action = 'No action required';
15 | $this->description = 'Checking for WordPress best practice';
16 | $this->score = 0;
17 | $this->result = '';
18 | $this->label = 'Best practice';
19 | $this->alerts = array();
20 | self::$instance = $this;
21 | return $this;
22 | }
23 |
24 | public function run() {
25 | $this->checkDebug();
26 | $this->checkCaching();
27 | $this->checkPluginCount();
28 | $this->checkUrls();
29 | $this->checkRegisteredDomains();
30 | $this->checkCoreUpdates();
31 | }
32 |
33 | public function checkCaching() {
34 | if (\is_plugin_active('w3-total-cache/w3-total-cache.php')) {
35 | $this->alerts[] = array(
36 | 'code' => 2,
37 | 'class' => 'warning',
38 | 'message' => 'W3 Total Cache plugin found. This plugin is not needed on Pantheon and should be removed.',
39 | );
40 | } else {
41 | $this->alerts[] = array(
42 | 'code' => 0,
43 | 'class' => 'ok',
44 | 'message' => 'W3 Total Cache not found.',
45 | );
46 | }
47 | if (\is_plugin_active('wp-super-cache/wp-cache.php')) {
48 | $this->alerts[] = array(
49 | 'code' => 2,
50 | 'class' => 'warning',
51 | 'message' => 'WP Super Cache plugin found. This plugin is not needed on Pantheon and should be removed.',
52 | );
53 | } else {
54 | $this->alerts[] = array(
55 | 'code' => 0,
56 | 'class' => 'ok',
57 | 'message' => 'WP Super Cache not found.',
58 | );
59 | }
60 |
61 | }
62 |
63 | public function checkURLS() {
64 | $siteurl = \get_option('siteurl');
65 | $home = \get_option('home');
66 | if ( $siteurl !== $home ) {
67 | $this->alerts[] = array(
68 | 'code' => 2,
69 | 'class' => 'error',
70 | 'message' => "Site url and home settings do not match. ( 'siteurl'=$siteurl and 'home'=>$home )",
71 | );
72 | } else {
73 | $this->alerts[] = array(
74 | 'code' => 0,
75 | 'class' => 'ok',
76 | 'message' => "Site and home url settings match. ( $siteurl )",
77 | );
78 | }
79 | }
80 |
81 | public function checkPluginCount() {
82 | $active = get_option('active_plugins');
83 | $plugins = count($active);
84 | if ( 100 <= $plugins ) {
85 | $this->alerts[] = array(
86 | 'code' => 1,
87 | 'class' => 'warning',
88 | 'message' => sprintf('%d active plugins found. You are running more than 100 plugins. The more plugins you run the worse your performance will be. You should uninstall any plugin that is not necessary.', $plugins),
89 | );
90 | } else {
91 | $this->alerts[] = array(
92 | 'code' => 0,
93 | 'class' => 'ok',
94 | 'message' => sprintf('%d active plugins found.',$plugins),
95 | );
96 | }
97 | }
98 |
99 | public function checkDebug() {
100 |
101 | if (defined('WP_DEBUG') AND WP_DEBUG ) {
102 | if (getenv('PANTHEON_ENVIRONMENT') AND 'live' === getenv('PANTHEON_ENVIRONMENT')) {
103 | $this->alerts[] = array(
104 | 'code' => 1,
105 | 'class' => 'warning',
106 | 'message' => 'The WP_DEBUG constant is set. You should not run debug mode in production.',
107 | );
108 | } else {
109 | $this->alerts[] = array(
110 | 'code' => 0,
111 | 'class' => 'ok',
112 | 'message' => 'The WP_DEBUG constant is set. You should remove this before deploying to live.',
113 | );
114 | }
115 | } else {
116 | $this->alerts[] = array(
117 | 'code' => 0,
118 | 'class' => 'ok',
119 | 'message' => 'WP_DEBUG not found or is set to false.',
120 | );
121 | }
122 |
123 | if (!function_exists('is_plugin_active')) {
124 | include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
125 | }
126 |
127 | if (\is_plugin_active('debug-bar/debug-bar.php')) {
128 | if (getenv('PANTHEON_ENVIRONMENT') AND 'live' === getenv('PANTHEON_ENVIRONMENT')) {
129 | $this->alerts[] = array(
130 | 'code' => 1,
131 | 'class' => 'warning',
132 | 'message' => 'Looks like you are running the debug bar plugin. You should disable this plugin in the live environment'
133 | );
134 | }
135 | }
136 |
137 | }
138 |
139 | public function checkRegisteredDomains() {
140 | if ( ! is_multisite() || ! function_exists( 'pantheon_curl' ) || empty( $_ENV['PANTHEON_ENVIRONMENT'] ) ) {
141 | return;
142 | }
143 | $bits = parse_url( 'https://api.live.getpantheon.com:8443/sites/self/state' );
144 | $response = pantheon_curl( sprintf( '%s://%s%s', $bits['scheme'], $bits['host'], $bits['path'] ), null, $bits['port'] );
145 | $body = ! empty( $response['body'] ) ? json_decode( $response['body'], true ) : '';
146 | $pantheon_domains = ! empty( $body['environments'][ $_ENV['PANTHEON_ENVIRONMENT'] ]['urls'] ) ? $body['environments'][ $_ENV['PANTHEON_ENVIRONMENT'] ]['urls'] : array();
147 | $site_domains = array();
148 | $it = new \WP_CLI\Iterators\Table( array(
149 | 'table' => $GLOBALS['wpdb']->blogs,
150 | ) );
151 | foreach( $it as $blog ) {
152 | $site_domains[] = parse_url( get_site_url( $blog->blog_id ), PHP_URL_HOST );
153 | }
154 | if ( $diff = array_diff( $site_domains, $pantheon_domains ) ) {
155 | $this->alerts[] = array(
156 | 'code' => 1,
157 | 'class' => 'warning',
158 | 'message' => 'One or more WordPress domains are not registered as Pantheon domains: ' . implode( ', ', $diff ),
159 | );
160 | } else {
161 | $this->alerts[] = array(
162 | 'code' => 0,
163 | 'class' => 'info',
164 | 'message' => 'WordPress domains are verified to be in sync with Pantheon domains.'
165 | );
166 | }
167 | }
168 |
169 | public function message(Messenger $messenger) {
170 | if (!empty($this->alerts)) {
171 | $total = 0;
172 | $rows = array();
173 | // this is dumb and left over from the previous iterationg. @TODO move scoring to run() method
174 | foreach ($this->alerts as $alert) {
175 | $total += $alert['code'];
176 | $rows[] = $alert;
177 | }
178 | $avg = $total/count($this->alerts);
179 | $this->result = View::make('checklist', array('rows'=> $rows) );
180 | $this->score = $avg;
181 | $this->action = $this->action ?? "You should use object caching";
182 | }
183 | $messenger->addMessage(get_object_vars($this));
184 | }
185 |
186 | public function checkCoreUpdates() {
187 | $updates = WP_CLI::runcommand( 'core check-update --format=json', array( 'return' => true, 'parse' => 'json' ) );
188 | $has_minor = $has_major = FALSE;
189 | foreach ($updates as $update) {
190 | switch ($update['update_type']) {
191 | case 'minor':
192 | $has_minor = TRUE;
193 | break;
194 | case 'major':
195 | $has_major = TRUE;
196 | break;
197 | }
198 | }
199 |
200 | if ( $has_minor ) {
201 | $action = "Updating to WordPress' newest minor version is strongly recommended.";
202 | $this->alerts[] = array(
203 | 'code' => 2,
204 | 'class' => 'error',
205 | 'message' => $action,
206 | );
207 | $this->action = $action;
208 | } else if ( $has_major ) {
209 | $action = 'A new major version of WordPress is available for update.';
210 | $this->alerts[] = array(
211 | 'code' => 1,
212 | 'class' => 'warning',
213 | 'message' => $action,
214 | );
215 | $this->action = $action;
216 | } else {
217 | $this->alerts[] = array(
218 | 'code' => 0,
219 | 'class' => 'ok',
220 | 'message' => 'WordPress is at the latest version.',
221 | );
222 | }
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/features/config.feature:
--------------------------------------------------------------------------------
1 | Feature: Check the wp-config.php file
2 |
3 | Scenario: WP Launch Check warns when WP_CACHE is defined to be true
4 | Given a WP install
5 | And a local-config.php file:
6 | """
7 | update your wp-config.php file to support $_ENV-based configuration values.
214 | """
215 |
216 | When I try `wp --require=wp-config-env.php launchcheck cron`
217 | Then STDERR should contain:
218 | """
219 | Error: Error establishing a database connection
220 | """
221 |
222 | When I try `wp --require=wp-config-env.php launchcheck all`
223 | Then STDOUT should contain:
224 | """
225 | Some database constants differ from their expected $_ENV values: DB_USER, DB_PASSWORD
226 | """
227 | And STDOUT should contain:
228 | """
229 | Recommendation: Please update your wp-config.php file to support $_ENV-based configuration values.
230 | """
231 | And STDERR should contain:
232 | """
233 | Warning: Detected invalid database credentials, skipping remaining checks
234 | """
235 | And STDERR should not contain:
236 | """
237 | Error: Error establishing a database connection
238 | """
239 |
--------------------------------------------------------------------------------
/features/general.feature:
--------------------------------------------------------------------------------
1 | Feature: General tests of WP Launch Check
2 |
3 | Scenario: WP Launch Check can be run from a non-ABSPATH directory
4 | Given a WP install
5 |
6 | When I run `cd wp-content; wp launchcheck cron`
7 | Then STDOUT should contain:
8 | """
9 | CRON: (Checking whether cron is enabled and what jobs are scheduled)
10 | """
11 |
12 | Scenario: General check warns when domains are mismatched
13 | Given a WP multisite subdomain install
14 | And a wp-content/mu-plugins/pantheon-setup.php file:
15 | """
16 | file_get_contents( dirname( __FILE__ ) . '/sample-data.json' ),
20 | );
21 | }
22 | $_ENV['PANTHEON_ENVIRONMENT'] = 'test';
23 | """
24 | And a wp-content/mu-plugins/sample-data.json file:
25 | """
26 | {"organization": false, "add_ons": [], "site": {"created": 1446035953, "created_by_user_id": "24980c36-de42-4e59-9b64-1061514fad74", "framework": "wordpress", "holder_id": "24980c36-de42-4e59-9b64-1061514fad74", "holder_type": "user", "last_code_push": {"timestamp": "2015-10-30T20:32:00", "user_uuid": null}, "name": "daniel-pantheon", "owner": "24980c36-de42-4e59-9b64-1061514fad74", "php_version": 55, "preferred_zone": "chios", "service_level": "free", "upstream": {"url": "https://github.com/pantheon-systems/WordPress", "product_id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf", "branch": "master"}, "label": "daniel-pantheon", "settings": {"allow_domains": false, "max_num_cdes": 10, "environment_styx_scheme": "https", "stunnel": false, "replica_verification_strategy": "legacy", "owner": "24980c36-de42-4e59-9b64-1061514fad74", "secure_runtime_access": false, "pingdom": 0, "allow_indexserver": false, "created_by_user_id": "24980c36-de42-4e59-9b64-1061514fad74", "failover_appserver": 0, "cacheserver": 1, "drush_version": 5, "label": "daniel-pantheon", "appserver": 1, "allow_read_slaves": false, "indexserver": 1, "php_version": 55, "php_channel": "stable", "allow_cacheserver": false, "ssl_enabled": null, "min_backups": 0, "service_level": "free", "dedicated_ip": null, "dbserver": 1, "framework": "wordpress", "upstream": {"url": "https://github.com/pantheon-systems/WordPress", "product_id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf", "branch": "master"}, "guilty_of_abuse": null, "preferred_zone": "chios", "pingdom_chance": 0, "holder_id": "24980c36-de42-4e59-9b64-1061514fad74", "name": "daniel-pantheon", "created": 1446035953, "max_backups": 0, "holder_type": "user", "number_allow_domains": 0, "pingdom_manually_enabled": false, "last_code_push": {"timestamp": "2015-10-30T20:32:00", "user_uuid": null}}, "base_domain": null}, "environments": {"dev": {"diffstat": {}, "allow_domains": false, "lock": {"username": null, "password": null, "locked": false}, "upstream": {"url": "https://github.com/pantheon-systems/WordPress", "product_id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf", "branch": "master"}, "environment_styx_scheme": "https", "stunnel": false, "target_ref": "refs/heads/master", "mysql": {"query_cache_size": 32, "innodb_buffer_pool_size": 128, "innodb_log_file_size": 50331648, "BlockIOWeight": 400, "MemoryLimit": 256, "CPUShares": 250}, "owner": "24980c36-de42-4e59-9b64-1061514fad74", "secure_runtime_access": false, "pingdom": 0, "guilty_of_abuse": null, "statuses": {}, "created_by_user_id": "24980c36-de42-4e59-9b64-1061514fad74", "failover_appserver": 0, "errors": {}, "cacheserver": 1, "on_server_development": true, "environment_created": 1446035953, "dns_zone": "pantheon.io", "schedule": {"0": null, "1": null, "2": null, "3": null, "4": null, "5": null, "6": null}, "redis": {"MemoryLimit": 64, "maxmemory": 52428800, "CPUShares": 8, "BlockIOWeight": 50}, "label": "daniel-pantheon", "environment": "dev", "appserver": 1, "number_allow_domains": 0, "allow_read_slaves": false, "indexserver": 1, "php_version": 55, "php_channel": "stable", "allow_cacheserver": false, "ssl_enabled": null, "styx_cluster": "styx-02.pantheon.io", "min_backups": 0, "service_level": "free", "dedicated_ip": null, "dbserver": 1, "site": "73cae74a-b66e-440a-ad3b-4f0679eb5e97", "framework": "wordpress", "holder_id": "24980c36-de42-4e59-9b64-1061514fad74", "max_num_cdes": 10, "allow_indexserver": false, "preferred_zone": "chios", "pingdom_chance": 0, "watchers": 0, "name": "daniel-pantheon", "created": 1446035953, "max_backups": 0, "php-fpm": {"fpm_max_children": 4, "opcache_revalidate_freq": 0, "BlockIOWeight": 100, "MemoryLimit": 512, "apc_shm_size": 128, "php_memory_limit": 256, "CPUShares": 250}, "randseed": "ZGZPLBXZUH63P6U6S6O0E9Q69A48L6GK", "last_code_push": {"timestamp": "2015-10-30T20:32:00", "user_uuid": null}, "loadbalancers": {}, "holder_type": "user", "replica_verification_strategy": "legacy", "urls": ["dev-daniel-pantheon.pantheon.io"], "target_commit": "f83daed591dc5c60425eef57092e6d374575bef5", "pingdom_manually_enabled": false, "nginx": {"sendfile": "off", "aio": "off", "worker_processes": 2, "directio": "off"}, "drush_version": 5}, "live": {"allow_domains": false, "lock": {"username": null, "password": null, "locked": false}, "upstream": {"url": "https://github.com/pantheon-systems/WordPress", "product_id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf", "branch": "master"}, "environment_styx_scheme": "https", "stunnel": false, "replica_verification_strategy": "legacy", "mysql": {"query_cache_size": 32, "innodb_buffer_pool_size": 128, "innodb_log_file_size": 50331648, "BlockIOWeight": 400, "MemoryLimit": 256, "CPUShares": 250}, "owner": "24980c36-de42-4e59-9b64-1061514fad74", "secure_runtime_access": false, "pingdom": 0, "guilty_of_abuse": null, "statuses": {}, "created_by_user_id": "24980c36-de42-4e59-9b64-1061514fad74", "failover_appserver": 0, "errors": {}, "cacheserver": 1, "loadbalancers": {}, "environment_created": 1446035954, "dns_zone": "pantheon.io", "schedule": {"0": null, "1": null, "2": null, "3": null, "4": null, "5": null, "6": null}, "redis": {"MemoryLimit": 64, "maxmemory": 52428800, "CPUShares": 8, "BlockIOWeight": 50}, "label": "daniel-pantheon", "environment": "live", "appserver": 1, "number_allow_domains": 0, "allow_read_slaves": false, "indexserver": 1, "php_version": 55, "php_channel": "stable", "allow_cacheserver": false, "ssl_enabled": null, "styx_cluster": "styx-01.pantheon.io", "service_level": "free", "dedicated_ip": null, "dbserver": 1, "site": "73cae74a-b66e-440a-ad3b-4f0679eb5e97", "framework": "wordpress", "max_num_cdes": 10, "allow_indexserver": false, "preferred_zone": "chios", "pingdom_chance": 0, "holder_id": "24980c36-de42-4e59-9b64-1061514fad74", "name": "daniel-pantheon", "created": 1446035953, "max_backups": 0, "php-fpm": {"fpm_max_children": 4, "opcache_revalidate_freq": 2, "BlockIOWeight": 100, "MemoryLimit": 512, "apc_shm_size": 128, "php_memory_limit": 256, "CPUShares": 250}, "randseed": "J1Y5E6VJHQ9CGNATQ9ZRW28XEATQVPX4", "last_code_push": {"timestamp": "2015-10-30T20:32:00", "user_uuid": null}, "holder_type": "user", "min_backups": 0, "urls": ["live-daniel-pantheon.pantheon.io"], "pingdom_manually_enabled": false, "nginx": {"sendfile": "off", "aio": "off", "worker_processes": 2, "directio": "off"}, "drush_version": 5}, "test": {"allow_domains": false, "lock": {"username": null, "password": null, "locked": false}, "upstream": {"url": "https://github.com/pantheon-systems/WordPress", "product_id": "e8fe8550-1ab9-4964-8838-2b9abdccf4bf", "branch": "master"}, "environment_styx_scheme": "https", "stunnel": false, "target_ref": "refs/tags/pantheon_test_2", "mysql": {"query_cache_size": 32, "innodb_buffer_pool_size": 128, "innodb_log_file_size": 50331648, "BlockIOWeight": 400, "MemoryLimit": 256, "CPUShares": 250}, "owner": "24980c36-de42-4e59-9b64-1061514fad74", "secure_runtime_access": false, "pingdom": 0, "guilty_of_abuse": null, "statuses": {}, "created_by_user_id": "24980c36-de42-4e59-9b64-1061514fad74", "failover_appserver": 0, "errors": {}, "cacheserver": 1, "loadbalancers": {}, "environment_created": 1446035954, "dns_zone": "pantheon.io", "schedule": {"0": null, "1": null, "2": null, "3": null, "4": null, "5": null, "6": null}, "redis": {"MemoryLimit": 64, "maxmemory": 52428800, "CPUShares": 8, "BlockIOWeight": 50}, "label": "daniel-pantheon", "environment": "test", "appserver": 1, "number_allow_domains": 0, "allow_read_slaves": false, "indexserver": 1, "php_version": 55, "php_channel": "stable", "allow_cacheserver": false, "ssl_enabled": null, "styx_cluster": "styx-03.pantheon.io", "min_backups": 0, "service_level": "free", "dedicated_ip": null, "dbserver": 1, "site": "73cae74a-b66e-440a-ad3b-4f0679eb5e97", "framework": "wordpress", "max_num_cdes": 10, "allow_indexserver": false, "preferred_zone": "chios", "pingdom_chance": 0, "holder_id": "24980c36-de42-4e59-9b64-1061514fad74", "name": "daniel-pantheon", "created": 1446035953, "max_backups": 0, "php-fpm": {"fpm_max_children": 4, "opcache_revalidate_freq": 2, "BlockIOWeight": 100, "MemoryLimit": 512, "apc_shm_size": 128, "php_memory_limit": 256, "CPUShares": 250}, "randseed": "7LU1MNL20EPAJ8XTN0FVF2B94BIKQ354", "last_code_push": {"timestamp": "2015-10-30T20:32:00", "user_uuid": null}, "holder_type": "user", "replica_verification_strategy": "legacy", "urls": ["test-daniel-pantheon.pantheon.io", "subsite.test-daniel-pantheon.pantheon.io"], "target_commit": "f83daed591dc5c60425eef57092e6d374575bef5", "pingdom_manually_enabled": false, "nginx": {"sendfile": "off", "aio": "off", "worker_processes": 2, "directio": "off"}, "drush_version": 5}}, "instrument": null}
27 | """
28 | And I run `wp site create --slug=subsite`
29 |
30 | When I run `wp launchcheck general`
31 | Then STDOUT should contain:
32 | """
33 | One or more WordPress domains are not registered as Pantheon domains: example.com, subsite.example.com
34 | """
35 |
36 | When I run `wp search-replace example.com test-daniel-pantheon.pantheon.io --network`
37 | Then STDOUT should not be empty
38 |
39 | When I run `wp launchcheck general --url=test-daniel-pantheon.pantheon.io`
40 | Then STDOUT should contain:
41 | """
42 | WordPress domains are verified to be in sync with Pantheon domains.
43 | """
44 |
45 | Scenario: WordPress is up-to-date
46 | Given a WP install
47 |
48 | When I run `wp core version`
49 | # This check is here to remind us to update versions when new releases are available.
50 | Then STDOUT should contain:
51 | """
52 | 6.7
53 | """
54 |
55 | When I run `wp launchcheck general`
56 | Then STDOUT should contain:
57 | """
58 | WordPress is at the latest version.
59 | """
60 |
61 | Scenario: WordPress has a new minor version but no new major version
62 | Given a WP install
63 | And I run `wp core download --version=6.7 --force`
64 | And I run `wp theme activate twentytwentyfive`
65 | And the current WP version is not the latest
66 |
67 | When I run `wp launchcheck general`
68 | Then STDOUT should contain:
69 | """
70 | Updating to WordPress' newest minor version is strongly recommended.
71 | """
72 |
73 | Scenario: WordPress has a new major version but no new minor version
74 | Given a WP install
75 | And I run `wp core download --version=6.6.2 --force`
76 | And I run `wp theme activate twentytwentytwo`
77 |
78 | When I run `wp launchcheck general`
79 | Then STDOUT should contain:
80 | """
81 | A new major version of WordPress is available for update.
82 | """
83 |
--------------------------------------------------------------------------------
/features/bootstrap/FeatureContext.php:
--------------------------------------------------------------------------------
1 | autoload->files ) ) {
31 | $contents = 'require:' . PHP_EOL;
32 | foreach( $composer->autoload->files as $file ) {
33 | $contents .= ' - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file;
34 | }
35 | @mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' );
36 | $project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml';
37 | file_put_contents( $project_config, $contents );
38 | putenv( 'WP_CLI_CONFIG_PATH=' . $project_config );
39 | }
40 | }
41 | // Inside WP-CLI
42 | } else {
43 | require_once __DIR__ . '/../../php/utils.php';
44 | require_once __DIR__ . '/../../php/WP_CLI/Process.php';
45 | }
46 |
47 | /**
48 | * Features context.
49 | */
50 | class FeatureContext extends BehatContext implements ClosuredContextInterface {
51 |
52 | private static $cache_dir, $suite_cache_dir;
53 |
54 | private static $db_settings = array(
55 | 'dbname' => 'pantheon',
56 | 'dbuser' => 'pantheon',
57 | 'dbpass' => 'pantheon',
58 | 'dbhost' => '127.0.0.1',
59 | );
60 |
61 | private $running_procs = array();
62 |
63 | public $variables = array();
64 |
65 | /**
66 | * Get the environment variables required for launched `wp` processes
67 | * @beforeSuite
68 | */
69 | private static function get_process_env_variables() {
70 | // Ensure we're using the expected `wp` binary
71 | $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . "/../../bin" );
72 | $env = array(
73 | 'PATH' => $bin_dir . ':' . getenv( 'PATH' ),
74 | 'BEHAT_RUN' => 1,
75 | 'HOME' => '/tmp/wp-cli-home',
76 | );
77 | if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) {
78 | $env['WP_CLI_CONFIG_PATH'] = $config_path;
79 | }
80 | return $env;
81 | }
82 |
83 | // We cache the results of `wp core download` to improve test performance
84 | // Ideally, we'd cache at the HTTP layer for more reliable tests
85 | private static function cache_wp_files() {
86 | self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test core-download-cache';
87 |
88 | if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) )
89 | return;
90 |
91 | $cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir );
92 | Process::create( $cmd, null, self::get_process_env_variables() )->run_check();
93 | }
94 |
95 | /**
96 | * @BeforeSuite
97 | */
98 | public static function prepare( SuiteEvent $event ) {
99 | $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check();
100 | echo PHP_EOL;
101 | echo $result->stdout;
102 | echo PHP_EOL;
103 | self::cache_wp_files();
104 | }
105 |
106 | /**
107 | * @AfterSuite
108 | */
109 | public static function afterSuite( SuiteEvent $event ) {
110 | if ( self::$suite_cache_dir ) {
111 | Process::create( Utils\esc_cmd( 'rm -r %s', self::$suite_cache_dir ), null, self::get_process_env_variables() )->run();
112 | }
113 | }
114 |
115 | /**
116 | * @BeforeScenario
117 | */
118 | public function beforeScenario( $event ) {
119 | $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' );
120 | }
121 |
122 | /**
123 | * @AfterScenario
124 | */
125 | public function afterScenario( $event ) {
126 | if ( isset( $this->variables['RUN_DIR'] ) ) {
127 | // remove altered WP install, unless there's an error
128 | if ( $event->getResult() < 4 ) {
129 | $this->proc( Utils\esc_cmd( 'rm -r %s', $this->variables['RUN_DIR'] ) )->run();
130 | }
131 | }
132 |
133 | foreach ( $this->running_procs as $proc ) {
134 | self::terminate_proc( $proc );
135 | }
136 | }
137 |
138 | /**
139 | * Terminate a process and any of its children.
140 | */
141 | private static function terminate_proc( $proc ) {
142 | $status = proc_get_status( $proc );
143 |
144 | $master_pid = $status['pid'];
145 |
146 | $output = `ps -o ppid,pid,command | grep $master_pid`;
147 |
148 | foreach ( explode( PHP_EOL, $output ) as $line ) {
149 | if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) {
150 | $parent = $matches[1];
151 | $child = $matches[2];
152 |
153 | if ( $parent == $master_pid ) {
154 | if ( ! posix_kill( (int) $child, 9 ) ) {
155 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) );
156 | }
157 | }
158 | }
159 | }
160 |
161 | if ( ! posix_kill( (int) $master_pid, 9 ) ) {
162 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) );
163 | }
164 | }
165 |
166 | public static function create_cache_dir() {
167 | self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-suite-cache-", TRUE );
168 | mkdir( self::$suite_cache_dir );
169 | return self::$suite_cache_dir;
170 | }
171 |
172 | /**
173 | * Initializes context.
174 | * Every scenario gets it's own context object.
175 | *
176 | * @param array $parameters context parameters (set them up through behat.yml)
177 | */
178 | public function __construct( array $parameters ) {
179 | $this->drop_db();
180 | $this->set_cache_dir();
181 | $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings );
182 | }
183 |
184 | public function getStepDefinitionResources() {
185 | return glob( __DIR__ . '/../steps/*.php' );
186 | }
187 |
188 | public function getHookDefinitionResources() {
189 | return array();
190 | }
191 |
192 | public function replace_variables( $str ) {
193 | return preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str );
194 | }
195 |
196 | private function _replace_var( $matches ) {
197 | $cmd = $matches[0];
198 |
199 | foreach ( array_slice( $matches, 1 ) as $key ) {
200 | $cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd );
201 | }
202 |
203 | return $cmd;
204 | }
205 |
206 | public function create_run_dir() {
207 | if ( !isset( $this->variables['RUN_DIR'] ) ) {
208 | $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-run-", TRUE );
209 | mkdir( $this->variables['RUN_DIR'] );
210 | }
211 | }
212 |
213 | public function build_phar( $version = 'same' ) {
214 | $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar';
215 |
216 | $this->proc( Utils\esc_cmd(
217 | 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s',
218 | __DIR__ . '/../../utils/make-phar.php',
219 | $this->variables['PHAR_PATH'],
220 | $version
221 | ) )->run_check();
222 | }
223 |
224 | private function set_cache_dir() {
225 | $path = sys_get_temp_dir() . '/wp-cli-test-cache';
226 | $this->proc( Utils\esc_cmd( 'mkdir -p %s', $path ) )->run_check();
227 | $this->variables['CACHE_DIR'] = $path;
228 | }
229 |
230 | private static function run_sql( $sql ) {
231 | Utils\run_mysql_command( 'mysql --no-defaults', array(
232 | 'execute' => $sql,
233 | 'host' => self::$db_settings['dbhost'],
234 | 'user' => self::$db_settings['dbuser'],
235 | 'pass' => self::$db_settings['dbpass'],
236 | ) );
237 | }
238 |
239 | public function create_db() {
240 | $dbname = self::$db_settings['dbname'];
241 | self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" );
242 | }
243 |
244 | public function drop_db() {
245 | $dbname = self::$db_settings['dbname'];
246 | self::run_sql( "DROP DATABASE IF EXISTS $dbname" );
247 | }
248 |
249 | public function proc( $command, $assoc_args = array(), $path = '' ) {
250 | if ( !empty( $assoc_args ) )
251 | $command .= Utils\assoc_args_to_str( $assoc_args );
252 |
253 | $env = self::get_process_env_variables();
254 | if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) {
255 | $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR'];
256 | }
257 |
258 | if ( isset( $this->variables['RUN_DIR'] ) ) {
259 | $cwd = "{$this->variables['RUN_DIR']}/{$path}";
260 | } else {
261 | $cwd = null;
262 | }
263 |
264 | return Process::create( $command, $cwd, $env );
265 | }
266 |
267 | /**
268 | * Start a background process. Will automatically be closed when the tests finish.
269 | */
270 | public function background_proc( $cmd ) {
271 | $descriptors = array(
272 | 0 => STDIN,
273 | 1 => array( 'pipe', 'w' ),
274 | 2 => array( 'pipe', 'w' ),
275 | );
276 |
277 | $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() );
278 |
279 | sleep(1);
280 |
281 | $status = proc_get_status( $proc );
282 |
283 | if ( !$status['running'] ) {
284 | throw new RuntimeException( stream_get_contents( $pipes[2] ) );
285 | } else {
286 | $this->running_procs[] = $proc;
287 | }
288 | }
289 |
290 | public function move_files( $src, $dest ) {
291 | rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" );
292 | }
293 |
294 | public function add_line_to_wp_config( &$wp_config_code, $line ) {
295 | $token = "/* That's all, stop editing!";
296 |
297 | $wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code );
298 | }
299 |
300 | public function download_wp( $subdir = '' ) {
301 | $dest_dir = $this->variables['RUN_DIR'] . "/$subdir";
302 |
303 | if ( $subdir ) {
304 | mkdir( $dest_dir );
305 | }
306 |
307 | $this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check();
308 |
309 | // disable emailing
310 | mkdir( $dest_dir . '/wp-content/mu-plugins' );
311 | copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' );
312 | }
313 |
314 | public function create_config( $subdir = '' ) {
315 | $params = self::$db_settings;
316 | $params['dbprefix'] = $subdir ?: 'wp_';
317 |
318 | $params['skip-salts'] = true;
319 | $this->proc( 'wp core config', $params, $subdir )->run_check();
320 | }
321 |
322 | public function install_wp( $subdir = '' ) {
323 | $this->create_db();
324 | $this->create_run_dir();
325 | $this->download_wp( $subdir );
326 |
327 | $this->create_config( $subdir );
328 |
329 | $install_args = array(
330 | 'url' => 'http://example.com',
331 | 'title' => 'WP CLI Site',
332 | 'admin_user' => 'admin',
333 | 'admin_email' => 'admin@example.com',
334 | 'admin_password' => 'password1'
335 | );
336 |
337 | $this->proc( 'wp core install', $install_args, $subdir )->run_check();
338 | }
339 | }
340 |
341 |
--------------------------------------------------------------------------------
/features/bootstrap/utils.php:
--------------------------------------------------------------------------------
1 | config ) && ! empty( $composer->config->{'vendor-dir'} ) ) {
66 | array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} );
67 | }
68 | }
69 | return $vendor_paths;
70 | }
71 |
72 | // Using require() directly inside a class grants access to private methods to the loaded code
73 | function load_file( $path ) {
74 | require_once $path;
75 | }
76 |
77 | function load_command( $name ) {
78 | $path = WP_CLI_ROOT . "/php/commands/$name.php";
79 |
80 | if ( is_readable( $path ) ) {
81 | include_once $path;
82 | }
83 | }
84 |
85 | function load_all_commands() {
86 | $cmd_dir = WP_CLI_ROOT . '/php/commands';
87 |
88 | $iterator = new \DirectoryIterator( $cmd_dir );
89 |
90 | foreach ( $iterator as $filename ) {
91 | if ( '.php' != substr( $filename, -4 ) )
92 | continue;
93 |
94 | include_once "$cmd_dir/$filename";
95 | }
96 | }
97 |
98 | /**
99 | * Like array_map(), except it returns a new iterator, instead of a modified array.
100 | *
101 | * Example:
102 | *
103 | * $arr = array('Football', 'Socker');
104 | *
105 | * $it = iterator_map($arr, 'strtolower', function($val) {
106 | * return str_replace('foo', 'bar', $val);
107 | * });
108 | *
109 | * foreach ( $it as $val ) {
110 | * var_dump($val);
111 | * }
112 | *
113 | * @param array|object Either a plain array or another iterator
114 | * @param callback The function to apply to an element
115 | * @return object An iterator that applies the given callback(s)
116 | */
117 | function iterator_map( $it, $fn ) {
118 | if ( is_array( $it ) ) {
119 | $it = new \ArrayIterator( $it );
120 | }
121 |
122 | if ( !method_exists( $it, 'add_transform' ) ) {
123 | $it = new Transform( $it );
124 | }
125 |
126 | foreach ( array_slice( func_get_args(), 1 ) as $fn ) {
127 | $it->add_transform( $fn );
128 | }
129 |
130 | return $it;
131 | }
132 |
133 | /**
134 | * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true
135 | * @param string|array The files (or file) to search for
136 | * @param string|null The directory to start searching from; defaults to CWD
137 | * @param callable Function which is passed the current dir each time a directory level is traversed
138 | * @return null|string Null if the file was not found
139 | */
140 | function find_file_upward( $files, $dir = null, $stop_check = null ) {
141 | $files = (array) $files;
142 | if ( is_null( $dir ) ) {
143 | $dir = getcwd();
144 | }
145 | while ( @is_readable( $dir ) ) {
146 | // Stop walking up when the supplied callable returns true being passed the $dir
147 | if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) {
148 | return null;
149 | }
150 |
151 | foreach ( $files as $file ) {
152 | $path = $dir . DIRECTORY_SEPARATOR . $file;
153 | if ( file_exists( $path ) ) {
154 | return $path;
155 | }
156 | }
157 |
158 | $parent_dir = dirname( $dir );
159 | if ( empty($parent_dir) || $parent_dir === $dir ) {
160 | break;
161 | }
162 | $dir = $parent_dir;
163 | }
164 | return null;
165 | }
166 |
167 | function is_path_absolute( $path ) {
168 | // Windows
169 | if ( isset($path[1]) && ':' === $path[1] )
170 | return true;
171 |
172 | return $path[0] === '/';
173 | }
174 |
175 | /**
176 | * Composes positional arguments into a command string.
177 | *
178 | * @param array
179 | * @return string
180 | */
181 | function args_to_str( $args ) {
182 | return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) );
183 | }
184 |
185 | /**
186 | * Composes associative arguments into a command string.
187 | *
188 | * @param array
189 | * @return string
190 | */
191 | function assoc_args_to_str( $assoc_args ) {
192 | $str = '';
193 |
194 | foreach ( $assoc_args as $key => $value ) {
195 | if ( true === $value )
196 | $str .= " --$key";
197 | else
198 | $str .= " --$key=" . escapeshellarg( $value );
199 | }
200 |
201 | return $str;
202 | }
203 |
204 | /**
205 | * Given a template string and an arbitrary number of arguments,
206 | * returns the final command, with the parameters escaped.
207 | */
208 | function esc_cmd( $cmd ) {
209 | if ( func_num_args() < 2 )
210 | trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING );
211 |
212 | $args = func_get_args();
213 |
214 | $cmd = array_shift( $args );
215 |
216 | return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) );
217 | }
218 |
219 | function locate_wp_config() {
220 | static $path;
221 |
222 | if ( null === $path ) {
223 | if ( file_exists( ABSPATH . 'wp-config.php' ) )
224 | $path = ABSPATH . 'wp-config.php';
225 | elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) )
226 | $path = ABSPATH . '../wp-config.php';
227 | else
228 | $path = false;
229 |
230 | if ( $path )
231 | $path = realpath( $path );
232 | }
233 |
234 | return $path;
235 | }
236 |
237 | function wp_version_compare( $since, $operator ) {
238 | return version_compare( str_replace( array( '-src' ), '', $GLOBALS['wp_version'] ), $since, $operator );
239 | }
240 |
241 | /**
242 | * Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count.
243 | *
244 | * Given a collection of items with a consistent data structure:
245 | *
246 | * ```
247 | * $items = array(
248 | * array(
249 | * 'key' => 'foo',
250 | * 'value' => 'bar',
251 | * )
252 | * );
253 | * ```
254 | *
255 | * Render `$items` as an ASCII table:
256 | *
257 | * ```
258 | * WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) );
259 | *
260 | * # +-----+-------+
261 | * # | key | value |
262 | * # +-----+-------+
263 | * # | foo | bar |
264 | * # +-----+-------+
265 | * ```
266 | *
267 | * Or render `$items` as YAML:
268 | *
269 | * ```
270 | * WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) );
271 | *
272 | * # ---
273 | * # -
274 | * # key: foo
275 | * # value: bar
276 | * ```
277 | *
278 | * @access public
279 | * @category Output
280 | *
281 | * @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count'
282 | * @param array $items An array of items to output.
283 | * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list.
284 | * @return null
285 | */
286 | function format_items( $format, $items, $fields ) {
287 | $assoc_args = compact( 'format', 'fields' );
288 | $formatter = new \WP_CLI\Formatter( $assoc_args );
289 | $formatter->display_items( $items );
290 | }
291 |
292 | /**
293 | * Write data as CSV to a given file.
294 | *
295 | * @access public
296 | *
297 | * @param resource $fd File descriptor
298 | * @param array $rows Array of rows to output
299 | * @param array $headers List of CSV columns (optional)
300 | */
301 | function write_csv( $fd, $rows, $headers = array() ) {
302 | if ( ! empty( $headers ) ) {
303 | fputcsv( $fd, $headers );
304 | }
305 |
306 | foreach ( $rows as $row ) {
307 | if ( ! empty( $headers ) ) {
308 | $row = pick_fields( $row, $headers );
309 | }
310 |
311 | fputcsv( $fd, array_values( $row ) );
312 | }
313 | }
314 |
315 | /**
316 | * Pick fields from an associative array or object.
317 | *
318 | * @param array|object Associative array or object to pick fields from
319 | * @param array List of fields to pick
320 | * @return array
321 | */
322 | function pick_fields( $item, $fields ) {
323 | $item = (object) $item;
324 |
325 | $values = array();
326 |
327 | foreach ( $fields as $field ) {
328 | $values[ $field ] = isset( $item->$field ) ? $item->$field : null;
329 | }
330 |
331 | return $values;
332 | }
333 |
334 | /**
335 | * Launch system's $EDITOR to edit text
336 | *
337 | * @param str $content Text to edit (eg post content)
338 | * @return str|bool Edited text, if file is saved from editor
339 | * False, if no change to file
340 | */
341 | function launch_editor_for_input( $input, $title = 'WP-CLI' ) {
342 |
343 | $tmpfile = wp_tempnam( $title );
344 |
345 | if ( !$tmpfile )
346 | \WP_CLI::error( 'Error creating temporary file.' );
347 |
348 | $output = '';
349 | file_put_contents( $tmpfile, $input );
350 |
351 | $editor = getenv( 'EDITOR' );
352 | if ( !$editor ) {
353 | if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) )
354 | $editor = 'notepad';
355 | else
356 | $editor = 'vi';
357 | }
358 |
359 | $descriptorspec = array( STDIN, STDOUT, STDERR );
360 | $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes );
361 | $r = proc_close( $process );
362 | if ( $r ) {
363 | exit( $r );
364 | }
365 |
366 | $output = file_get_contents( $tmpfile );
367 |
368 | unlink( $tmpfile );
369 |
370 | if ( $output === $input )
371 | return false;
372 |
373 | return $output;
374 | }
375 |
376 | /**
377 | * @param string MySQL host string, as defined in wp-config.php
378 | * @return array
379 | */
380 | function mysql_host_to_cli_args( $raw_host ) {
381 | $assoc_args = array();
382 |
383 | $host_parts = explode( ':', $raw_host );
384 | if ( count( $host_parts ) == 2 ) {
385 | list( $assoc_args['host'], $extra ) = $host_parts;
386 | $extra = trim( $extra );
387 | if ( is_numeric( $extra ) ) {
388 | $assoc_args['port'] = intval( $extra );
389 | $assoc_args['protocol'] = 'tcp';
390 | } else if ( $extra !== '' ) {
391 | $assoc_args['socket'] = $extra;
392 | }
393 | } else {
394 | $assoc_args['host'] = $raw_host;
395 | }
396 |
397 | return $assoc_args;
398 | }
399 |
400 | function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) {
401 | if ( !$descriptors )
402 | $descriptors = array( STDIN, STDOUT, STDERR );
403 |
404 | if ( isset( $assoc_args['host'] ) ) {
405 | $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) );
406 | }
407 |
408 | $pass = $assoc_args['pass'];
409 | unset( $assoc_args['pass'] );
410 |
411 | $old_pass = getenv( 'MYSQL_PWD' );
412 | putenv( 'MYSQL_PWD=' . $pass );
413 |
414 | $final_cmd = $cmd . assoc_args_to_str( $assoc_args );
415 |
416 | $proc = proc_open( $final_cmd, $descriptors, $pipes );
417 | if ( !$proc )
418 | exit(1);
419 |
420 | $r = proc_close( $proc );
421 |
422 | putenv( 'MYSQL_PWD=' . $old_pass );
423 |
424 | if ( $r ) exit( $r );
425 | }
426 |
427 | /**
428 | * Render PHP or other types of files using Mustache templates.
429 | *
430 | * IMPORTANT: Automatic HTML escaping is disabled!
431 | */
432 | function mustache_render( $template_name, $data ) {
433 | if ( ! file_exists( $template_name ) )
434 | $template_name = WP_CLI_ROOT . "/templates/$template_name";
435 |
436 | $template = file_get_contents( $template_name );
437 |
438 | $m = new \Mustache_Engine( array(
439 | 'escape' => function ( $val ) { return $val; }
440 | ) );
441 |
442 | return $m->render( $template, $data );
443 | }
444 |
445 | /**
446 | * Create a progress bar to display percent completion of a given operation.
447 | *
448 | * Progress bar is written to STDOUT, and disabled when command is piped. Progress
449 | * advances with `$progress->tick()`, and completes with `$progress->finish()`.
450 | * Process bar also indicates elapsed time and expected total time.
451 | *
452 | * ```
453 | * # `wp user generate` ticks progress bar each time a new user is created.
454 | * #
455 | * # $ wp user generate --count=500
456 | * # Generating users 22 % [=======> ] 0:05 / 0:23
457 | *
458 | * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count );
459 | * for ( $i = 0; $i < $count; $i++ ) {
460 | * // uses wp_insert_user() to insert the user
461 | * $progress->tick();
462 | * }
463 | * $progress->finish();
464 | * ```
465 | *
466 | * @access public
467 | * @category Output
468 | *
469 | * @param string $message Text to display before the progress bar.
470 | * @param integer $count Total number of ticks to be performed.
471 | * @return cli\progress\Bar|WP_CLI\NoOp
472 | */
473 | function make_progress_bar( $message, $count ) {
474 | if ( \cli\Shell::isPiped() )
475 | return new \WP_CLI\NoOp;
476 |
477 | return new \cli\progress\Bar( $message, $count );
478 | }
479 |
480 | function parse_url( $url ) {
481 | $url_parts = \parse_url( $url );
482 |
483 | if ( !isset( $url_parts['scheme'] ) ) {
484 | $url_parts = parse_url( 'http://' . $url );
485 | }
486 |
487 | return $url_parts;
488 | }
489 |
490 | /**
491 | * Check if we're running in a Windows environment (cmd.exe).
492 | */
493 | function is_windows() {
494 | return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
495 | }
496 |
497 | /**
498 | * Replace magic constants in some PHP source code.
499 | *
500 | * @param string $source The PHP code to manipulate.
501 | * @param string $path The path to use instead of the magic constants
502 | */
503 | function replace_path_consts( $source, $path ) {
504 | $replacements = array(
505 | '__FILE__' => "'$path'",
506 | '__DIR__' => "'" . dirname( $path ) . "'"
507 | );
508 |
509 | $old = array_keys( $replacements );
510 | $new = array_values( $replacements );
511 |
512 | return str_replace( $old, $new, $source );
513 | }
514 |
515 | /**
516 | * Make a HTTP request to a remote URL.
517 | *
518 | * Wraps the Requests HTTP library to ensure every request includes a cert.
519 | *
520 | * ```
521 | * # `wp core download` verifies the hash for a downloaded WordPress archive
522 | *
523 | * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' );
524 | * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) {
525 | * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" );
526 | * }
527 | * ```
528 | *
529 | * @access public
530 | *
531 | * @param string $method HTTP method (GET, POST, DELETE, etc.)
532 | * @param string $url URL to make the HTTP request to.
533 | * @param array $headers Add specific headers to the request.
534 | * @param array $options
535 | * @return object
536 | */
537 | function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) {
538 |
539 | $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem';
540 | if ( inside_phar() ) {
541 | // cURL can't read Phar archives
542 | $options['verify'] = extract_from_phar(
543 | WP_CLI_ROOT . '/vendor' . $cert_path );
544 | } else {
545 | foreach( get_vendor_paths() as $vendor_path ) {
546 | if ( file_exists( $vendor_path . $cert_path ) ) {
547 | $options['verify'] = $vendor_path . $cert_path;
548 | break;
549 | }
550 | }
551 | if ( empty( $options['verify'] ) ){
552 | WP_CLI::error_log( "Cannot find SSL certificate." );
553 | }
554 | }
555 |
556 | try {
557 | $request = \Requests::request( $url, $headers, $data, $method, $options );
558 | return $request;
559 | } catch( \Requests_Exception $ex ) {
560 | // Handle SSL certificate issues gracefully
561 | \WP_CLI::warning( $ex->getMessage() );
562 | $options['verify'] = false;
563 | try {
564 | return \Requests::request( $url, $headers, $data, $method, $options );
565 | } catch( \Requests_Exception $ex ) {
566 | \WP_CLI::error( $ex->getMessage() );
567 | }
568 | }
569 | }
570 |
571 | /**
572 | * Increments a version string using the "x.y.z-pre" format
573 | *
574 | * Can increment the major, minor or patch number by one
575 | * If $new_version == "same" the version string is not changed
576 | * If $new_version is not a known keyword, it will be used as the new version string directly
577 | *
578 | * @param string $current_version
579 | * @param string $new_version
580 | * @return string
581 | */
582 | function increment_version( $current_version, $new_version ) {
583 | // split version assuming the format is x.y.z-pre
584 | $current_version = explode( '-', $current_version, 2 );
585 | $current_version[0] = explode( '.', $current_version[0] );
586 |
587 | switch ( $new_version ) {
588 | case 'same':
589 | // do nothing
590 | break;
591 |
592 | case 'patch':
593 | $current_version[0][2]++;
594 |
595 | $current_version = array( $current_version[0] ); // drop possible pre-release info
596 | break;
597 |
598 | case 'minor':
599 | $current_version[0][1]++;
600 | $current_version[0][2] = 0;
601 |
602 | $current_version = array( $current_version[0] ); // drop possible pre-release info
603 | break;
604 |
605 | case 'major':
606 | $current_version[0][0]++;
607 | $current_version[0][1] = 0;
608 | $current_version[0][2] = 0;
609 |
610 | $current_version = array( $current_version[0] ); // drop possible pre-release info
611 | break;
612 |
613 | default: // not a keyword
614 | $current_version = array( array( $new_version ) );
615 | break;
616 | }
617 |
618 | // reconstruct version string
619 | $current_version[0] = implode( '.', $current_version[0] );
620 | $current_version = implode( '-', $current_version );
621 |
622 | return $current_version;
623 | }
624 |
625 | /**
626 | * Compare two version strings to get the named semantic version.
627 | *
628 | * @access public
629 | *
630 | * @param string $new_version
631 | * @param string $original_version
632 | * @return string $name 'major', 'minor', 'patch'
633 | */
634 | function get_named_sem_ver( $new_version, $original_version ) {
635 |
636 | if ( ! Comparator::greaterThan( $new_version, $original_version ) ) {
637 | return '';
638 | }
639 |
640 | $parts = explode( '-', $original_version );
641 | list( $major, $minor, $patch ) = explode( '.', $parts[0] );
642 |
643 | if ( Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) {
644 | return 'patch';
645 | } else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) {
646 | return 'minor';
647 | } else {
648 | return 'major';
649 | }
650 | }
651 |
652 | /**
653 | * Return the flag value or, if it's not set, the $default value.
654 | *
655 | * Because flags can be negated (e.g. --no-quiet to negate --quiet), this
656 | * function provides a safer alternative to using
657 | * `isset( $assoc_args['quiet'] )` or similar.
658 | *
659 | * @access public
660 | * @category Input
661 | *
662 | * @param array $assoc_args Arguments array.
663 | * @param string $flag Flag to get the value.
664 | * @param mixed $default Default value for the flag. Default: NULL
665 | * @return mixed
666 | */
667 | function get_flag_value( $assoc_args, $flag, $default = null ) {
668 | return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default;
669 | }
670 |
671 | /**
672 | * Get the system's temp directory. Warns user if it isn't writable.
673 | *
674 | * @access public
675 | * @category System
676 | *
677 | * @return string
678 | */
679 | function get_temp_dir() {
680 | static $temp = '';
681 |
682 | $trailingslashit = function( $path ) {
683 | return rtrim( $path ) . '/';
684 | };
685 |
686 | if ( $temp )
687 | return $trailingslashit( $temp );
688 |
689 | if ( function_exists( 'sys_get_temp_dir' ) ) {
690 | $temp = sys_get_temp_dir();
691 | } else if ( ini_get( 'upload_tmp_dir' ) ) {
692 | $temp = ini_get( 'upload_tmp_dir' );
693 | } else {
694 | $temp = '/tmp/';
695 | }
696 |
697 | if ( ! @is_writable( $temp ) ) {
698 | WP_CLI::warning( "Temp directory isn't writable: {$temp}" );
699 | }
700 |
701 | return $trailingslashit( $temp );
702 | }
703 |
704 | /**
705 | * Get the latest WordPress version from the version check API endpoint.
706 | *
707 | * @access public
708 | * @category System
709 | * @throws Exception If the version check API fails to respond.
710 | * @return string
711 | */
712 | function get_wp_version() {
713 | // Fetch the latest WordPress version info from the WordPress.org API
714 | $url = 'https://api.wordpress.org/core/version-check/1.7/';
715 | $context = stream_context_create(['http' => ['timeout' => 5]]);
716 | $json = file_get_contents($url, false, $context);
717 | if ($json === false) {
718 | throw new \Exception('Failed to fetch the latest WordPress version.');
719 | }
720 |
721 | $data = json_decode($json, true);
722 |
723 | // Extract the latest version number
724 | $latestVersion = $data['offers'][0]['current'];
725 | return trim($latestVersion);
726 | }
727 |
--------------------------------------------------------------------------------