Mini Shell

Direktori : /proc/self/root/opt/sharedrads/check_software_mods/
Upload File :
Current File : //proc/self/root/opt/sharedrads/check_software_mods/wordpress.py

"""WordPress module for check_software"""

from pathlib import Path
import re
from typing import Union
from packaging import version as pkg_version
import requests
import pymysql
from pymysql.cursors import Cursor
from check_software_mods.template import ModTemplate


class Module(ModTemplate):
    """WordPress module"""

    @classmethod
    @property
    def config_file(cls):
        return 'wp-config.php'

    @classmethod
    @property
    def cms_name(cls):
        return 'WordPress'

    @staticmethod
    def is_config(path: Path) -> bool:
        """if the filename is wp-config.php, we assume yes"""
        return path.name == 'wp-config.php'

    def scan_install(self, conf_path: Path):
        site_path = conf_path.parent

        # Read wp-config.php database configuration
        try:
            prefix, db_conf = self.parse_config(conf_path)
        except (OSError, KeyError) as exc:
            self.red(f"{type(exc).__name__}: {exc}")
            self.red(f'Site at {site_path} cannot be scanned')
            return
        # Read version.php site version
        try:
            site_version = self.get_version(site_path)
        except (OSError, ValueError) as exc:
            self.red(f"{type(exc).__name__}: {exc}")
            site_version = ''

        # Connect to the database and collect variables
        try:
            with pymysql.connect(host='localhost', **db_conf) as db_conn:
                with db_conn.cursor() as cur:
                    db_data = self.get_database_data(cur, prefix)
        except pymysql.Error as exc:
            self.red(f"{type(exc).__name__}: {exc}")
            self.red(f'Site at {site_path} cannot be scanned')
            return

        self.print_site_config(db_data, site_version, site_path)
        if not db_data['plugin_paths']:
            self.blue('No active plugins')
            return
        self.blue('List of active plugins')
        for plugin in db_data['plugin_paths']:
            self.show_plugin(site_path, plugin)

    def print_site_config(
        self, db_data: dict, site_version: str, site_path: Path
    ):
        """Print general information on a WordPress site"""
        pad = 12
        self.green('Name:'.ljust(pad), end='')
        self.bold(db_data.get('blogname', '?'))

        self.green('URL:'.ljust(pad), end='')
        self.bold(db_data.get('siteurl', '?'))

        self.green('Path:'.ljust(pad), end='')
        self.bold(str(site_path))

        self.green('Version:'.ljust(pad), end='')
        if not LATEST_WP or not site_version:
            self.bold(site_version or '?')
        else:
            if pkg_version.parse(LATEST_WP) > pkg_version.parse(site_version):
                self.red(f'{site_version} ({LATEST_WP} available)', end='')
                if self.args.style == 'str':
                    how = self.urlize(
                        self.kb_urls["wp_update"], 'How to update WordPress'
                    )
                    self.print(f' - {how}')
                else:
                    self.print('')
            else:
                self.green(site_version)

        self.green('Theme:'.ljust(pad), end='')
        self.bold(db_data.get('current_theme', '?'))

        self.green('Cache:'.ljust(pad), end='')
        self.check_wp_cache(site_path, db_data['plugin_paths'])

        self.green('Comments:'.ljust(pad), end='')
        if db_data.get('num_comments', 0) > 10000:
            self.red(str(db_data['num_comments']), end='')
            if self.args.style == 'str':
                how = self.urlize(
                    self.kb_urls['wp_spam'], 'How to moderate comments'
                )
                self.print(f'- {how}')
            else:
                self.print('')
        else:
            self.bold(str(db_data.get('num_comments', '?')))

        if 'multisites' in db_data:
            self.green('Multisite:'.ljust(pad), end='')
            self.red(str(db_data['multisites']))

        self.green('Plugins:'.ljust(pad), end='')
        if len(db_data['plugin_paths']) > 20:
            self.red(str(len(db_data['plugin_paths'])))
        else:
            self.bold(str(len(db_data['plugin_paths'])))

        self.green('List of Admin Users:')
        for user in db_data['admin_users']:
            self.bold(f"  {user}")

    @staticmethod
    def get_version(site_path: Path) -> str:
        """Obtain WordPress version"""
        site_version = None
        version_path = site_path / 'wp-includes/version.php'
        with open(version_path, encoding='utf-8') as version_file:
            data = version_file.read().splitlines()
        for line in data:
            if '$wp_version = ' in line:
                site_version = re.sub('[^\\w|\\.]', '', line.split(' = ')[1])
        if site_version is None:
            raise ValueError(f'version not found in {version_path}')
        return site_version

    def get_database_data(self, cur: Cursor, prefix: str) -> dict:
        """Collect WordPress database variables"""
        tbl = lambda x: f"{prefix}{x}".replace('`', '``')
        cur.execute(
            f"SELECT `option_name`, `option_value` FROM `{tbl('options')}` "
            "WHERE `option_name` IN "
            "('active_plugins', 'siteurl', 'blogname', 'current_theme')"
        )
        ret = dict(cur.fetchall())
        if active_plugins := ret.pop('active_plugins', ''):
            ret['plugin_paths'] = active_plugins.split('"')[1::2]
        else:
            ret['plugin_paths'] = []
        cur.execute(f"SELECT COUNT(*) FROM `{tbl('comments')}`")
        if row := cur.fetchone():
            try:
                ret['num_comments'] = int(row[0])
            except ValueError:
                pass
        try:
            cur.execute(
                f"SELECT `meta_value` FROM `{tbl('sitemeta')}` "
                "WHERE `meta_key` = 'blog_count'"
            )
            if row := cur.fetchone():
                ret['multisites'] = row[0]
        except pymysql.ProgrammingError:
            pass  # sitemeta table doesn't exist
        # supply {prefix}capabilities as a query arg instead of using `tbl()`
        # because it's a literal value, not a MySQL identifier
        cur.execute(
            f"SELECT u.user_login FROM `{tbl('users')}` AS u "
            f"LEFT JOIN `{tbl('usermeta')}` AS um ON u.ID = um.user_id "
            "WHERE um.meta_key = %s AND um.meta_value LIKE %s",
            (f"{prefix}capabilities", r'%admin%'),
        )
        ret['admin_users'] = [x[0] for x in cur.fetchall()]
        return ret

    def parse_config(self, path: Path) -> tuple[str, dict[str, str]]:
        """Parse database variables from a wp-config.php file"""
        db_re = re.compile(
            r"[^\#]*define\(\s*\'DB\_([A-Z]+)\'\s*\,\s*\'([^\']+)\'"
        )
        prefix_re = re.compile(r"[^#]*\$table_prefix\s*=\s*\'([^\']+)\'")
        db_info = {}
        prefix = None
        with open(path, encoding='utf-8') as conf_file:
            conf = conf_file.read().splitlines()
        for line in conf:
            if db_match := db_re.match(line):  # database setting found
                db_info[db_match.group(1)] = db_match.group(2)
            if prefix_match := prefix_re.match(line):  # table prefix found
                prefix = prefix_match.group(1)
        if not prefix:
            raise KeyError(f"Could not find $table_prefix in {path}")
        try:
            if self.args.use_root:
                conn_kwargs = {
                    'user': 'root',
                    'read_default_file': '/root/.my.cnf',
                    'database': db_info['NAME'],
                }
                return prefix, conn_kwargs
            conn_kwargs = {
                'user': db_info['USER'],
                'password': db_info['PASSWORD'],
                'database': db_info['NAME'],
            }
            return prefix, conn_kwargs
        except KeyError as exc:
            raise KeyError(f"Could not find DB_{exc} in {path}") from exc

    def show_plugin(self, site_path: Path, plugin_path: str) -> None:
        path = site_path / 'wp-content/plugins' / plugin_path
        try:
            with open(path, encoding='utf-8', errors='replace') as conf_file:
                conf = reversed(conf_file.read().splitlines())
        except OSError:
            self.yellow(f"Found active but missing plugin at {path}")
            return
        slug = slug_from_path(plugin_path)
        latest = latest_plugin_version(slug)
        version, uri, name = '', '', ''
        for line in conf:
            if 'Plugin Name:' in line:
                line = line.split(':')
                name = line[1].strip()
            elif 'Version:' in line:
                line = line.split(':')
                version = line[1].strip()
            elif 'Plugin URI:' in line:
                line = line.split(':')
                del line[0]
                uri = ':'.join(line).strip()
        if latest and version:
            outdated = pkg_version.parse(version) < pkg_version.parse(latest)
        else:  # missing data; cannot determine if out of date
            outdated = False
        if slug in GOOD_PLUGINS:
            comment = GOOD_PLUGINS[slug]
            self.green(f"  {name}", end='')
        elif slug in BAD_PLUGINS:
            comment = BAD_PLUGINS[slug]
            self.red(f"  {name}", end='')
        else:
            comment = ''
            self.bold(f"  {name}", end='')
        self.print(' - ', end='')
        self.yellow(version, end=' ')
        if outdated:
            self.print(' - ', end='')
            self.red(f"{latest} available", end='')
            if self.args.style == 'str' and slug not in BAD_PLUGINS:
                how = self.urlize(
                    self.kb_urls["wp_pluginup"], 'How to update plugins'
                )
                self.print(f' - {how}', end='')
        if comment:
            self.print(' - ', end='')
            self.yellow(comment, end='')
        self.print('')
        if self.args.style != 'str':
            self.print(f"    {uri}")

    def check_wp_cache(self, site_path: Path, plugins: list[str]) -> None:
        w3tc_enabled = 'w3-total-cache/w3-total-cache.php' in plugins
        wpsc_enabled = 'wp-super-cache/wp-cache.php' in plugins
        w3tc_browser_cache, w3tc_page_cache, wpsc_rewrites = False, None, False
        try:
            with open(site_path / '.htaccess', encoding='utf-8') as htaccess:
                for line in htaccess:
                    if 'BEGIN W3TC Browser Cache' in line:
                        w3tc_browser_cache = True
                    if 'BEGIN WPSuperCache' in line:
                        wpsc_rewrites = True
        except OSError:
            pass
        master_path = site_path / 'wp-content/w3tc-config/master.php'
        try:
            with open(master_path, encoding='utf-8') as php:
                for line in php:
                    if "pgcache.enabled" in line:
                        line = line.lower()
                        if 'true' in line:
                            w3tc_page_cache = True
                        elif 'false' in line:
                            w3tc_page_cache = False
                        break
        except OSError:
            pass
        if w3tc_enabled and wpsc_enabled:
            self.red(
                'W3 Total Cache AND WP Super Cache are enabled. '
                'They are incompatible'
            )
            return
        if w3tc_enabled:
            if wpsc_rewrites:
                self.red(
                    'WP Super Cache rewrites found, but W3 Total Cache '
                    'is enabled'
                )
                return
            self.green('W3TC enabled:', end=' ')
            show_kb = False
            if w3tc_browser_cache:
                self.green('W3TC Browser Cache rewrites found', end=' ')
            else:
                self.red('W3TC Browser Cache rewrites disabled', end=' ')
                show_kb = True
            if w3tc_page_cache:
                self.green('and W3TC Page Cache enabled', end='')
            elif w3tc_page_cache is None:
                self.red('but could not detect W3TC Page Cache state', end='')
            else:
                self.red('and W3TC Page Cache disabled', end='')
                show_kb = True
            if show_kb and self.args.style == 'str':
                how = self.urlize(
                    self.kb_urls['w3_total'],
                    'Setting up W3 Total Cache',
                )
                self.print(f" - {how}")
            else:
                self.print('')
            return
        if wpsc_enabled:
            self.green("WP Super Cache enabled", end=' ')
            if w3tc_page_cache or w3tc_browser_cache:
                self.red("but W3TC rewrites were found")
                return
            if wpsc_rewrites:
                self.green('and rewrites enabled')
            else:
                self.red('but missing rewrites', end='')
                if self.args.style == 'str':
                    how = self.urlize(
                        self.kb_urls['wp_super'], 'Setting up WP Super Cache'
                    )
                    self.print(f' - {how}')
                else:
                    self.print('')
            return
        self.red("No approved caching enabled", end='')
        if self.args.style == 'str':
            how = self.urlize(
                self.kb_urls['w3_total'], 'Setting up W3 Total Cache'
            )
            self.print(f' - {how}')
        else:
            self.print('')
        if self.args.guide_bool:
            self.args.wp_found = True
            self.red("    **See below for caching instructions")


def latest_wp_ver():
    """Determine the latest WordPress version"""
    try:
        api_response = requests.get(
            'https://api.wordpress.org/core/version-check/1.7/',
            timeout=10.0,
        ).json()
    except (ValueError, TypeError, requests.exceptions.RequestException):
        return None
    try:
        return str(
            max(pkg_version.parse(x['current']) for x in api_response['offers'])
        )
    except (KeyError, TypeError, ValueError):
        return None


def slug_from_path(plugin_path: str) -> str:
    """From a plugin path, obtain a WordPress 'slug'
    http://codex.wordpress.org/Glossary#Slug"""
    # Expected format of plugin_path:
    #  Complex plugins will be in their own directory, and file_path
    #   will point to their entry script, like
    #   'w3-total-cache/w3-total-cache.php'. For these, we want the
    #   'w3-total-cache' part in the beginning
    #  Simple plugins will be directly in wp-content/plugins, such
    #   as 'hello.php' (hello dolly). For these, we want the first
    #   'hello' part in the beginning.
    if plugin_path.endswith('.php') and len(plugin_path) > 4:
        slug = plugin_path[:-4]
    else:
        slug = plugin_path
    slug = slug.lstrip('/')
    if '/' in slug:
        slug = slug[: slug.index('/')]
    return slug


def latest_plugin_version(slug: str) -> Union[str, None]:
    """Determine the latest version of a plugin"""
    try:
        return LATEST_PLUGINS[slug]
    except KeyError:
        pass
    try:
        ver = requests.get(
            f'https://api.wordpress.org/plugins/info/1.0/{slug}.json',
            timeout=10.0,
        ).json()['version']
        if ver:
            LATEST_PLUGINS[slug] = ver
        return ver
    except (
        ValueError,
        TypeError,
        KeyError,
        requests.exceptions.RequestException,
    ):
        return None


def load_plugin_data() -> tuple[dict[str, str], dict[str, str]]:
    data = requests.get(
        'http://repo.imhadmin.net/open/control/wordpress.json',
        timeout=10.0,
    ).json()
    return data['good'], data['bad']


LATEST_WP = latest_wp_ver()
if not LATEST_WP:
    print('Warning: could not determine latest WordPress version')
LATEST_PLUGINS: dict[str, str] = {}
GOOD_PLUGINS, BAD_PLUGINS = load_plugin_data()

Zerion Mini Shell 1.0