4cc3f5bee4
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
- Convert tabs to spaces (3,413 violations) - Fix line endings, trailing whitespace, brace placement - Break lines exceeding 150-char absolute limit - Replace heredoc tab closers with spaces - Fix empty elseif, forbidden function calls - Update phpcs.xml: exclude rules inappropriate for CLI scripts (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder, empty catch blocks) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
690 lines
20 KiB
PHP
690 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Enterprise.Plugins
|
|
* INGROUP: MokoStandards
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /lib/Enterprise/Plugins/WordPressPlugin.php
|
|
* BRIEF: Enterprise plugin for WordPress projects
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MokoEnterprise\Plugins;
|
|
|
|
use MokoEnterprise\AbstractProjectPlugin;
|
|
|
|
/**
|
|
* WordPress Project Plugin
|
|
*
|
|
* Provides validation, metrics, and management capabilities for
|
|
* WordPress plugins and themes.
|
|
*/
|
|
class WordPressPlugin extends AbstractProjectPlugin
|
|
{
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getProjectType(): string
|
|
{
|
|
return 'wordpress';
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getPluginName(): string
|
|
{
|
|
return 'WordPress Enterprise Plugin';
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function validateProject(array $config, string $projectPath): array
|
|
{
|
|
$errors = [];
|
|
$warnings = [];
|
|
|
|
$projectTypeWP = $this->detectWordPressType($projectPath);
|
|
|
|
// Check for main file
|
|
$mainFile = $this->findMainFile($projectPath, $projectTypeWP);
|
|
if (!$mainFile) {
|
|
$errors[] = 'No WordPress plugin or theme header found';
|
|
} else {
|
|
$headerData = $this->parseHeader($mainFile, $projectTypeWP);
|
|
if (!$headerData) {
|
|
$errors[] = 'Invalid WordPress header format';
|
|
} else {
|
|
if (empty($headerData['name'])) {
|
|
$errors[] = 'Missing plugin/theme name in header';
|
|
}
|
|
if (empty($headerData['version'])) {
|
|
$warnings[] = 'Missing version in header';
|
|
}
|
|
if (empty($headerData['author'])) {
|
|
$warnings[] = 'Missing author in header';
|
|
}
|
|
if ($projectTypeWP === 'plugin' && empty($headerData['license'])) {
|
|
$warnings[] = 'Missing license in header';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for WordPress coding standards
|
|
if (
|
|
!$this->fileExists($projectPath, 'phpcs.xml') &&
|
|
!$this->fileExists($projectPath, 'phpcs.xml.dist')
|
|
) {
|
|
$warnings[] = 'No PHPCS configuration found (WordPress Coding Standards recommended)';
|
|
}
|
|
|
|
// Check for text domain
|
|
if (!$this->hasTextDomain($projectPath)) {
|
|
$warnings[] = 'No text domain found for translations';
|
|
}
|
|
|
|
// Check for unescaped output (basic check)
|
|
if ($this->hasUnescapedOutput($projectPath)) {
|
|
$warnings[] = 'Potential unescaped output found (security risk)';
|
|
}
|
|
|
|
// Check for direct file access protection
|
|
if (!$this->hasFileAccessProtection($projectPath)) {
|
|
$warnings[] = 'Some files missing direct access protection';
|
|
}
|
|
|
|
$this->log(
|
|
'WordPress project validation completed',
|
|
'info',
|
|
['errors' => count($errors), 'warnings' => count($warnings), 'type' => $projectTypeWP]
|
|
);
|
|
|
|
return [
|
|
'valid' => empty($errors),
|
|
'errors' => $errors,
|
|
'warnings' => $warnings,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function collectMetrics(string $projectPath, array $config): array
|
|
{
|
|
$projectTypeWP = $this->detectWordPressType($projectPath);
|
|
|
|
$metrics = [
|
|
'wordpress_type' => $projectTypeWP,
|
|
'php_files' => $this->countFiles($projectPath, '**/*.php'),
|
|
'js_files' => $this->countFiles($projectPath, '**/*.js'),
|
|
'css_files' => $this->countFiles($projectPath, '**/*.css'),
|
|
'template_files' => $this->countTemplateFiles($projectPath, $projectTypeWP),
|
|
'has_hooks' => $this->hasHooks($projectPath),
|
|
'hooks_count' => $this->countHooks($projectPath),
|
|
'has_ajax' => $this->hasAjax($projectPath),
|
|
'has_rest_api' => $this->hasRestAPI($projectPath),
|
|
'has_gutenberg_blocks' => $this->hasGutenbergBlocks($projectPath),
|
|
'has_widgets' => $this->hasWidgets($projectPath),
|
|
'has_shortcodes' => $this->hasShortcodes($projectPath),
|
|
'has_tests' => $this->fileExists($projectPath, 'tests') ||
|
|
$this->fileExists($projectPath, 'test'),
|
|
];
|
|
|
|
// Count lines of code
|
|
$phpFiles = $this->findFiles($projectPath, '**/*.php');
|
|
$totalLines = 0;
|
|
foreach ($phpFiles as $file) {
|
|
if (is_file($file)) {
|
|
$totalLines += count(file($file));
|
|
}
|
|
}
|
|
$metrics['total_lines'] = $totalLines;
|
|
|
|
// Record metrics
|
|
$this->recordMetric('wordpress', 'php_files', $metrics['php_files']);
|
|
$this->recordMetric('wordpress', 'total_lines', $totalLines);
|
|
$this->recordMetric('wordpress', 'hooks_count', $metrics['hooks_count']);
|
|
|
|
$this->log('Collected WordPress metrics', 'info', $metrics);
|
|
|
|
return $metrics;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function healthCheck(string $projectPath, array $config): array
|
|
{
|
|
$issues = [];
|
|
$score = 100;
|
|
|
|
$projectTypeWP = $this->detectWordPressType($projectPath);
|
|
|
|
// Check for main file
|
|
$mainFile = $this->findMainFile($projectPath, $projectTypeWP);
|
|
if (!$mainFile) {
|
|
$issues[] = [
|
|
'severity' => 'critical',
|
|
'message' => 'No WordPress plugin or theme header found',
|
|
];
|
|
$score -= 30;
|
|
}
|
|
|
|
// Check for security issues
|
|
if ($this->hasUnescapedOutput($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Potential unescaped output detected',
|
|
];
|
|
$score -= 15;
|
|
}
|
|
|
|
if (!$this->hasFileAccessProtection($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Some files missing direct access protection',
|
|
];
|
|
$score -= 10;
|
|
}
|
|
|
|
// Check for SQL injection risks
|
|
if ($this->hasSQLInjectionRisk($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'critical',
|
|
'message' => 'Potential SQL injection vulnerability detected',
|
|
];
|
|
$score -= 20;
|
|
}
|
|
|
|
// Check for nonce verification
|
|
if (!$this->hasNonceVerification($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Missing nonce verification in forms/AJAX',
|
|
];
|
|
$score -= 10;
|
|
}
|
|
|
|
// Check for text domain
|
|
if (!$this->hasTextDomain($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'No text domain for translations',
|
|
];
|
|
$score -= 5;
|
|
}
|
|
|
|
// Check for README
|
|
if (
|
|
!$this->fileExists($projectPath, 'README.md') &&
|
|
!$this->fileExists($projectPath, 'readme.txt')
|
|
) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Missing README file',
|
|
];
|
|
$score -= 5;
|
|
}
|
|
|
|
// Check for license
|
|
if (
|
|
!$this->fileExists($projectPath, 'LICENSE') &&
|
|
!$this->fileExists($projectPath, 'license.txt')
|
|
) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Missing LICENSE file',
|
|
];
|
|
$score -= 5;
|
|
}
|
|
|
|
$score = max(0, $score);
|
|
|
|
$this->log('WordPress health check completed', 'info', [
|
|
'score' => $score,
|
|
'issues_count' => count($issues),
|
|
'type' => $projectTypeWP,
|
|
]);
|
|
|
|
return [
|
|
'healthy' => $score >= 70,
|
|
'score' => $score,
|
|
'issues' => $issues,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getRequiredFiles(): array
|
|
{
|
|
return [
|
|
'Plugin: main plugin file with header',
|
|
'Theme: style.css with theme header',
|
|
'Theme: index.php',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getRecommendedFiles(): array
|
|
{
|
|
return [
|
|
'README.md or readme.txt',
|
|
'LICENSE or license.txt',
|
|
'CHANGELOG.md',
|
|
'phpcs.xml or phpcs.xml.dist',
|
|
'languages/*.pot (translation template)',
|
|
'assets/ (for WordPress.org)',
|
|
'uninstall.php (for cleanup)',
|
|
'Plugin: plugin-name.php',
|
|
'Theme: functions.php, screenshot.png',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getConfigSchema(): array
|
|
{
|
|
return [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'wordpress_type' => [
|
|
'type' => 'string',
|
|
'enum' => ['plugin', 'theme', 'mu-plugin'],
|
|
'description' => 'Type of WordPress project',
|
|
],
|
|
'min_wp_version' => [
|
|
'type' => 'string',
|
|
'description' => 'Minimum WordPress version required',
|
|
],
|
|
'min_php_version' => [
|
|
'type' => 'string',
|
|
'description' => 'Minimum PHP version required',
|
|
],
|
|
'text_domain' => [
|
|
'type' => 'string',
|
|
'description' => 'Text domain for translations',
|
|
],
|
|
'uses_gutenberg' => [
|
|
'type' => 'boolean',
|
|
'description' => 'Uses Gutenberg blocks',
|
|
],
|
|
],
|
|
'required' => ['wordpress_type'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getBestPractices(): array
|
|
{
|
|
return [
|
|
'Follow WordPress Coding Standards',
|
|
'Use proper escaping for all output (esc_html, esc_attr, etc.)',
|
|
'Sanitize all user input',
|
|
'Use $wpdb->prepare() for database queries',
|
|
'Implement nonce verification for forms and AJAX',
|
|
'Add direct file access protection to all PHP files',
|
|
'Use wp_enqueue_script/style for assets',
|
|
'Implement proper text domain for translations',
|
|
'Use WordPress APIs instead of direct database access',
|
|
'Add uninstall.php for cleanup',
|
|
'Follow semantic versioning',
|
|
'Include comprehensive inline documentation',
|
|
'Use hooks (actions/filters) for extensibility',
|
|
'Implement proper error handling and logging',
|
|
'Test with WP_DEBUG enabled',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Detect WordPress project type
|
|
*/
|
|
private function detectWordPressType(string $projectPath): string
|
|
{
|
|
// Check for theme
|
|
if ($this->fileExists($projectPath, 'style.css')) {
|
|
$styleContent = $this->readFile($projectPath, 'style.css');
|
|
if ($styleContent && strpos($styleContent, 'Theme Name:') !== false) {
|
|
return 'theme';
|
|
}
|
|
}
|
|
|
|
// Check for plugin
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
foreach ($phpFiles as $file) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && strpos($content, 'Plugin Name:') !== false) {
|
|
return 'plugin';
|
|
}
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Find main file
|
|
*/
|
|
private function findMainFile(string $projectPath, string $type): ?string
|
|
{
|
|
if ($type === 'theme') {
|
|
$styleFile = $projectPath . '/style.css';
|
|
return file_exists($styleFile) ? $styleFile : null;
|
|
}
|
|
|
|
// Look for plugin header
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
foreach ($phpFiles as $file) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && strpos($content, 'Plugin Name:') !== false) {
|
|
return $file;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Parse WordPress header
|
|
*/
|
|
private function parseHeader(string $file, string $type): ?array
|
|
{
|
|
$content = @file_get_contents($file);
|
|
if (!$content) {
|
|
return null;
|
|
}
|
|
|
|
$data = [
|
|
'name' => null,
|
|
'version' => null,
|
|
'author' => null,
|
|
'license' => null,
|
|
];
|
|
|
|
$nameField = $type === 'theme' ? 'Theme Name' : 'Plugin Name';
|
|
|
|
if (preg_match('/' . $nameField . ':\s*(.+)/i', $content, $matches)) {
|
|
$data['name'] = trim($matches[1]);
|
|
}
|
|
if (preg_match('/Version:\s*(.+)/i', $content, $matches)) {
|
|
$data['version'] = trim($matches[1]);
|
|
}
|
|
if (preg_match('/Author:\s*(.+)/i', $content, $matches)) {
|
|
$data['author'] = trim($matches[1]);
|
|
}
|
|
if (preg_match('/License:\s*(.+)/i', $content, $matches)) {
|
|
$data['license'] = trim($matches[1]);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Check for text domain
|
|
*/
|
|
private function hasTextDomain(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach (array_slice($phpFiles, 0, 5) as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && preg_match('/__(|_e|_x|_ex|_n)\s*\(/', $content)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check for unescaped output
|
|
*/
|
|
private function hasUnescapedOutput(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach (array_slice($phpFiles, 0, 10) as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content) {
|
|
// Look for echo without escape functions
|
|
if (preg_match('/echo\s+\$[^;]+;(?!.*esc_)/m', $content)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check for file access protection
|
|
*/
|
|
private function hasFileAccessProtection(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
$protectedCount = 0;
|
|
|
|
foreach (array_slice($phpFiles, 0, 10) as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if (
|
|
$content && (
|
|
strpos($content, 'defined( \'ABSPATH\' )') !== false ||
|
|
strpos($content, 'defined(\'ABSPATH\')') !== false ||
|
|
strpos($content, 'if ( ! defined( \'ABSPATH\' ) )') !== false
|
|
)
|
|
) {
|
|
$protectedCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $protectedCount > count(array_slice($phpFiles, 0, 10)) / 2;
|
|
}
|
|
|
|
/**
|
|
* Check for SQL injection risk
|
|
*/
|
|
private function hasSQLInjectionRisk(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach (array_slice($phpFiles, 0, 10) as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content) {
|
|
// Look for direct $wpdb->query without prepare
|
|
if (preg_match('/\$wpdb->query\s*\(\s*["\'].*\$/', $content)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check for nonce verification
|
|
*/
|
|
private function hasNonceVerification(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach (array_slice($phpFiles, 0, 10) as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if (
|
|
$content && (
|
|
strpos($content, 'wp_verify_nonce') !== false ||
|
|
strpos($content, 'check_ajax_referer') !== false
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Count template files
|
|
*/
|
|
private function countTemplateFiles(string $projectPath, string $type): int
|
|
{
|
|
if ($type === 'theme') {
|
|
return $this->countFiles($projectPath, '*.php');
|
|
}
|
|
|
|
return $this->countFiles($projectPath, 'templates/*.php');
|
|
}
|
|
|
|
/**
|
|
* Check for hooks
|
|
*/
|
|
private function hasHooks(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach (array_slice($phpFiles, 0, 5) as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if (
|
|
$content && (
|
|
strpos($content, 'add_action') !== false ||
|
|
strpos($content, 'add_filter') !== false
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Count hooks
|
|
*/
|
|
private function countHooks(string $projectPath): int
|
|
{
|
|
$count = 0;
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach ($phpFiles as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content) {
|
|
$count += preg_match_all('/(add_action|add_filter)\s*\(/', $content);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Check for AJAX
|
|
*/
|
|
private function hasAjax(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach ($phpFiles as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && strpos($content, 'wp_ajax_') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check for REST API
|
|
*/
|
|
private function hasRestAPI(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach ($phpFiles as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && strpos($content, 'register_rest_route') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check for Gutenberg blocks
|
|
*/
|
|
private function hasGutenbergBlocks(string $projectPath): bool
|
|
{
|
|
return $this->fileExists($projectPath, 'blocks') ||
|
|
$this->fileExists($projectPath, 'src/blocks') ||
|
|
$this->countFiles($projectPath, '**/block.json') > 0;
|
|
}
|
|
|
|
/**
|
|
* Check for widgets
|
|
*/
|
|
private function hasWidgets(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach ($phpFiles as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && strpos($content, 'WP_Widget') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check for shortcodes
|
|
*/
|
|
private function hasShortcodes(string $projectPath): bool
|
|
{
|
|
$phpFiles = $this->findFiles($projectPath, '*.php');
|
|
|
|
foreach ($phpFiles as $file) {
|
|
if (is_file($file)) {
|
|
$content = @file_get_contents($file);
|
|
if ($content && strpos($content, 'add_shortcode') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|