├── .travis.yml ├── LICENSE ├── composer.json └── src ├── Db.php ├── HookAnnotation.php ├── HookConstructorTo.php ├── HookInitTo.php ├── HookProxy.php ├── Is.php ├── SiteInfo.php └── helpers.php /.travis.yml: -------------------------------------------------------------------------------- 1 | # TravisCI configuration for szepeviktor/toolkit4wp 2 | 3 | if: "branch = master" 4 | 5 | language: "php" 6 | arch: 7 | - "amd64" 8 | - "arm64-graviton2" 9 | os: 10 | - "linux" 11 | dist: "bionic" 12 | 13 | php: 14 | - "7.4" 15 | - "7.1" 16 | 17 | jobs: 18 | include: 19 | - name: "List outdated dependencies" 20 | arch: "arm64-graviton2" 21 | php: "7.4" 22 | script: "composer outdated --no-interaction --direct" 23 | 24 | cache: 25 | directories: 26 | - "${HOME}/.composer/cache" 27 | 28 | before_install: 29 | - "composer validate --no-interaction --strict" 30 | 31 | install: 32 | - "composer update --no-interaction" 33 | 34 | script: 35 | - "composer run-script --no-interaction test" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Viktor Szépe 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "szepeviktor/toolkit4wp", 3 | "description": "OOP toolkit for daily tasks in WordPress development.", 4 | "keywords": [ 5 | "tools", 6 | "oop", 7 | "wordpress" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": ">=7.1" 12 | }, 13 | "require-dev": { 14 | "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.6.1", 15 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7", 16 | "szepeviktor/phpstan-wordpress": "^1.0.2" 17 | }, 18 | "suggest": { 19 | }, 20 | "config": { 21 | "optimize-autoloader": true, 22 | "allow-plugins": { 23 | "dealerdirect/phpcodesniffer-composer-installer": true 24 | } 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Toolkit4WP\\": "src/" 29 | }, 30 | "files": [ 31 | "src/helpers.php" 32 | ] 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Toolkit4WP\\Tests\\": "tests/" 37 | } 38 | }, 39 | "scripts": { 40 | "cs": [ 41 | "@phpcs:set-php-version", 42 | "@phpcs:run" 43 | ], 44 | "phpcs:run": "phpcs -s --standard=PSR12NeutronRuleset src/", 45 | "phpcs:set-php-version": "phpcs --config-set php_version 70100", 46 | "phpstan": "phpstan analyze", 47 | "syntax": "find -L . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n 1 -P 4 -- php -l", 48 | "test": [ 49 | "@composer validate --strict", 50 | "@syntax", 51 | "@phpstan", 52 | "@cs" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Db.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | // phpcs:disable NeutronStandard.MagicMethods.DisallowMagicGet.MagicGet,NeutronStandard.MagicMethods.DisallowMagicSet.MagicSet,NeutronStandard.MagicMethods.RiskyMagicMethod.RiskyMagicMethod 16 | 17 | /** 18 | * Connect to global $wpdb instance from OOP code. 19 | * 20 | * Usage example. 21 | * 22 | * $db = new \Toolkit4WP\Db(); $db->prepare('...'); 23 | * 24 | * @see https://www.php.net/manual/en/language.oop5.magic.php 25 | */ 26 | class Db 27 | { 28 | /** 29 | * Get a property. 30 | * 31 | * @see https://codex.wordpress.org/Class_Reference/wpdb#Class_Variables 32 | * @param string $name 33 | * @return mixed 34 | */ 35 | public function __get(string $name) 36 | { 37 | global $wpdb; 38 | 39 | return $wpdb->$name; 40 | } 41 | 42 | /** 43 | * Noop on set. 44 | * 45 | * @param string $name 46 | * @param mixed $value 47 | * @return void 48 | * 49 | * phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter 50 | */ 51 | public function __set(string $name, $value): void 52 | { 53 | } 54 | // phpcs:enable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter 55 | 56 | /** 57 | * Execute a method. 58 | * 59 | * @see https://www.php.net/manual/en/language.oop5.overloading.php#object.call 60 | * @param string $name 61 | * @param array $arguments 62 | * @return mixed 63 | */ 64 | public function __call(string $name, array $arguments) 65 | { 66 | global $wpdb; 67 | 68 | $callback = [$wpdb, $name]; 69 | if (! \is_callable($callback)) { 70 | throw new \InvalidArgumentException('Unknown wpdb method: ' . $name); 71 | } 72 | 73 | // phpcs:ignore NeutronStandard.Functions.DisallowCallUserFunc.CallUserFunc 74 | return \call_user_func_array($callback, $arguments); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/HookAnnotation.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | use ReflectionClass; 16 | use ReflectionMethod; 17 | 18 | use function add_filter; 19 | 20 | /** 21 | * Implement hooking in method annotation. 22 | * 23 | * Format: @hook tag 10 24 | * @hook tag first 25 | * @hook tag last 26 | * 27 | * mindplay/annotations may be a better solution. 28 | * 29 | * @see https://github.com/szepeviktor/debian-server-tools/blob/master/webserver/wordpress/WordPress-hooks.md 30 | */ 31 | trait HookAnnotation 32 | { 33 | protected function hookMethods(int $defaultPriority = 10): void 34 | { 35 | $classReflection = new ReflectionClass(self::class); 36 | // Look for hook tag in all public methods. 37 | foreach ($classReflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 38 | // Do not hook constructor, use HookConstructorTo. 39 | if ($method->isConstructor()) { 40 | continue; 41 | } 42 | $hookDetails = $this->getMetadata((string)$method->getDocComment(), $defaultPriority); 43 | if ($hookDetails === null) { 44 | continue; 45 | } 46 | 47 | add_filter( 48 | $hookDetails['tag'], 49 | [$this, $method->name], 50 | $hookDetails['priority'], 51 | $method->getNumberOfParameters() 52 | ); 53 | } 54 | } 55 | 56 | /** 57 | * Read hook tag from docblock. 58 | * 59 | * @return array{tag: string, priority: int}|null 60 | */ 61 | protected function getMetadata(string $docComment, int $defaultPriority): ?array 62 | { 63 | $matches = []; 64 | if ( 65 | \preg_match( 66 | // @hook ( tag ) ( priority ) 67 | '/^\s+\*\s+@hook\s+([\w\/_=-]+)(?:\s+(\d+|first|last))?\s*$/m', 68 | $docComment, 69 | $matches 70 | ) !== 1 71 | ) { 72 | return null; 73 | } 74 | 75 | if (! isset($matches[2])) { 76 | return [ 77 | 'tag' => $matches[1], 78 | 'priority' => $defaultPriority, 79 | ]; 80 | } 81 | 82 | switch ($matches[2]) { 83 | case 'first': 84 | $priority = PHP_INT_MIN; 85 | break; 86 | case 'last': 87 | $priority = PHP_INT_MAX; 88 | break; 89 | default: 90 | $priority = \intval($matches[2]); 91 | break; 92 | } 93 | 94 | return [ 95 | 'tag' => $matches[1], 96 | 'priority' => $priority, 97 | ]; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/HookConstructorTo.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | use ReflectionClass; 16 | 17 | use function add_filter; 18 | 19 | /** 20 | * Hook class constructor on to a specific action. 21 | * 22 | * Example call with priority zero. 23 | * 24 | * HookConstructorTo::{'acf/init'}(MyClass::class, 0); 25 | */ 26 | class HookConstructorTo 27 | { 28 | protected const DEFAULT_PRIORITY = 10; 29 | 30 | /** 31 | * Hook to the action in the method name. 32 | * 33 | * @param string $actionTag 34 | * @param array $arguments = [ 35 | * @type class-string $class 36 | * @type int $pritority 37 | * ] 38 | */ 39 | public static function __callStatic(string $actionTag, array $arguments): void 40 | { 41 | if ($arguments === []) { 42 | throw new \ArgumentCountError('Class name must be supplied.'); 43 | } 44 | 45 | // phpcs:ignore SlevomatCodingStandard.PHP.RequireExplicitAssertion.RequiredExplicitAssertion 46 | /** @var class-string $class */ 47 | $class = $arguments[0]; 48 | 49 | $constructor = (new ReflectionClass($class))->getConstructor(); 50 | if ($constructor === null) { 51 | throw new \ErrorException('The class must have a constructor defined.'); 52 | } 53 | 54 | // Hook the constructor. 55 | add_filter( 56 | $actionTag, 57 | static function () use ($class): void { 58 | // Pass hook parameters to constructor. 59 | $args = func_get_args(); 60 | // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction 61 | new $class(...$args); 62 | }, 63 | \intval($arguments[1] ?? self::DEFAULT_PRIORITY), 64 | $constructor->getNumberOfParameters() 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/HookInitTo.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | use ReflectionClass; 16 | 17 | use function add_filter; 18 | 19 | /** 20 | * Hook init() method on to a specific action. 21 | * 22 | * Example call with priority zero. 23 | * 24 | * HookInitTo::plugins_loaded(MyClass::class, 0); 25 | */ 26 | class HookInitTo 27 | { 28 | protected const DEFAULT_PRIORITY = 10; 29 | 30 | /** 31 | * Hook to the action in the method name. 32 | * 33 | * @param string $actionTag 34 | * @param array $arguments = [ 35 | * @type class-string $class 36 | * @type int $pritority 37 | * ] 38 | * @throws \ArgumentCountError 39 | * @throws \ReflectionException 40 | */ 41 | public static function __callStatic(string $actionTag, array $arguments): void 42 | { 43 | if ($arguments === []) { 44 | throw new \ArgumentCountError('Class name must be supplied.'); 45 | } 46 | 47 | // phpcs:ignore SlevomatCodingStandard.PHP.RequireExplicitAssertion.RequiredExplicitAssertion 48 | /** @var class-string $class */ 49 | $class = $arguments[0]; 50 | 51 | $initMethod = (new ReflectionClass($class))->getMethod('init'); 52 | 53 | // Hook 'init' method. 54 | add_filter( 55 | $actionTag, 56 | static function () use ($class): void { 57 | // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction 58 | $instance = new $class(); 59 | // Pass hook parameters to init() 60 | $args = func_get_args(); 61 | $instance->init(...$args); 62 | }, 63 | \intval($arguments[1] ?? self::DEFAULT_PRIORITY), 64 | $initMethod->getNumberOfParameters() 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/HookProxy.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | use Closure; 16 | use ReflectionClass; 17 | use ReflectionMethod; 18 | 19 | use function _wp_filter_build_unique_id; 20 | use function add_filter; 21 | use function remove_filter; 22 | 23 | /** 24 | * Implement lazy hooking. 25 | */ 26 | trait HookProxy 27 | { 28 | use HookAnnotation; 29 | 30 | /** @var array */ 31 | protected $callablesAdded; 32 | 33 | protected function lazyHookFunction( 34 | string $actionTag, 35 | callable $callable, 36 | int $priority, 37 | int $argumentCount, 38 | string $filePath 39 | ): void { 40 | add_filter( 41 | $actionTag, 42 | $this->generateClosureWithFileLoad($actionTag, $callable, $filePath), 43 | $priority, 44 | $argumentCount 45 | ); 46 | } 47 | 48 | protected function lazyHookStaticMethod( 49 | string $actionTag, 50 | callable $callable, 51 | int $priority, 52 | int $argumentCount 53 | ): void { 54 | add_filter( 55 | $actionTag, 56 | $this->generateClosure($actionTag, $callable), 57 | $priority, 58 | $argumentCount 59 | ); 60 | } 61 | 62 | protected function lazyHookMethod( 63 | string $actionTag, 64 | callable $callable, 65 | int $priority, 66 | int $argumentCount, 67 | ?callable $injector = null 68 | ): void { 69 | add_filter( 70 | $actionTag, 71 | $this->generateClosureWithInjector($actionTag, $callable, $injector), 72 | $priority, 73 | $argumentCount 74 | ); 75 | } 76 | 77 | /** 78 | * This is not really lazy hooking as class must be loaded to use reflections. 79 | * 80 | * @param class-string $className 81 | */ 82 | protected function lazyHookAllMethods( 83 | string $className, 84 | int $defaultPriority = 10, 85 | ?callable $injector = null 86 | ): void { 87 | $classReflection = new ReflectionClass($className); 88 | // Look for hook tag in all public methods. 89 | foreach ($classReflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 90 | // Do not hook constructor. 91 | if ($method->isConstructor()) { 92 | continue; 93 | } 94 | $hookDetails = $this->getMetadata((string)$method->getDocComment(), $defaultPriority); 95 | if ($hookDetails === null) { 96 | continue; 97 | } 98 | 99 | add_filter( 100 | $hookDetails['tag'], 101 | $this->generateClosureWithInjector($hookDetails['tag'], [$className, $method->name], $injector), 102 | $hookDetails['priority'], 103 | $method->getNumberOfParameters() 104 | ); 105 | } 106 | } 107 | 108 | protected function unhook( 109 | string $actionTag, 110 | callable $callable, 111 | int $priority 112 | ): void { 113 | $id = $this->buildUniqueId($actionTag, $callable); 114 | if (! array_key_exists($id, $this->callablesAdded)) { 115 | return; 116 | } 117 | 118 | remove_filter( 119 | $actionTag, 120 | $this->callablesAdded[$id], 121 | $priority 122 | ); 123 | unset($this->callablesAdded[$id]); 124 | } 125 | 126 | // phpcs:disable NeutronStandard.Functions.TypeHint.NoReturnType 127 | 128 | protected function generateClosure(string $actionTag, callable $callable): Closure 129 | { 130 | $id = $this->buildUniqueId($actionTag, $callable); 131 | $this->callablesAdded[$id] = static function (...$args) use ($callable) { 132 | return call_user_func_array($callable, $args); 133 | }; 134 | 135 | return $this->callablesAdded[$id]; 136 | } 137 | 138 | protected function generateClosureWithFileLoad(string $actionTag, callable $callable, string $filePath): Closure 139 | { 140 | $id = $this->buildUniqueId($actionTag, $callable); 141 | $this->callablesAdded[$id] = static function (...$args) use ($filePath, $callable) { 142 | require_once $filePath; 143 | 144 | return call_user_func_array($callable, $args); 145 | }; 146 | 147 | return $this->callablesAdded[$id]; 148 | } 149 | 150 | protected function generateClosureWithInjector(string $actionTag, callable $callable, ?callable $injector): Closure 151 | { 152 | if (! is_array($callable)) { 153 | throw new \InvalidArgumentException('Callable is not an array: ' . var_export($callable, true)); 154 | } 155 | 156 | $id = $this->buildUniqueId($actionTag, $callable); 157 | $this->callablesAdded[$id] = $injector === null 158 | ? static function (...$args) use ($callable) { 159 | return call_user_func_array($callable, $args); 160 | } 161 | : static function (...$args) use ($injector, $callable) { 162 | $instance = call_user_func($injector, $callable[0]); 163 | 164 | return call_user_func_array([$instance, $callable[1]], $args); 165 | }; 166 | 167 | return $this->callablesAdded[$id]; 168 | } 169 | 170 | protected function buildUniqueId(string $actionTag, callable $callable): string 171 | { 172 | return sprintf('%s/%s', $actionTag, _wp_filter_build_unique_id('', $callable, 0)); 173 | } 174 | } 175 | // TODO Measurements: w/o OPcache, OPcache with file read, OPcache without file read 176 | // TODO Add tests, remove_action, usage as filter with returned value, 177 | // one callable hooked to many action tags then removed 178 | -------------------------------------------------------------------------------- /src/Is.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | use WP_User; 16 | 17 | /** 18 | * Various request helpers. 19 | * 20 | * @see https://github.com/chesio/bc-security/blob/develop/classes/BlueChip/Security/Helpers/Is.php 21 | */ 22 | class Is 23 | { 24 | /** 25 | * Whether we are in a live environment. 26 | * 27 | * @return bool 28 | */ 29 | public static function live(): bool 30 | { 31 | // Consider both production and staging environment as live. 32 | return \defined('WP_ENV') && \in_array(\WP_ENV, ['production', 'staging'], true); 33 | } 34 | 35 | /** 36 | * Whether given user is an administrator. 37 | * 38 | * @param \WP_User $user The given user. 39 | * @return bool 40 | */ 41 | public static function admin(WP_User $user): bool 42 | { 43 | return \is_multisite() ? \user_can($user, 'manage_network') : \user_can($user, 'manage_options'); 44 | } 45 | 46 | /** 47 | * Whether the current user is not logged in. 48 | * 49 | * @return bool 50 | */ 51 | public static function anonymousUsers(): bool 52 | { 53 | return ! \is_user_logged_in(); 54 | } 55 | 56 | /** 57 | * Whether the current user is a comment author. 58 | * 59 | * @return bool 60 | */ 61 | public static function commentAuthor(): bool 62 | { 63 | // phpcs:ignore WordPress.VIP.RestrictedVariables.cache_constraints___COOKIE 64 | return isset($_COOKIE['comment_author_' . \COOKIEHASH]); 65 | } 66 | 67 | /** 68 | * Whether current webserver interface is CLI. 69 | * 70 | * @return bool 71 | */ 72 | public static function cli(): bool 73 | { 74 | return \php_sapi_name() === 'cli'; 75 | } 76 | 77 | /** 78 | * Whether current request is of the given type. 79 | * 80 | * All of them are available even before 'muplugins_loaded' action, 81 | * exceptions are commented. 82 | * 83 | * @param string $type Type of request. 84 | * @return bool 85 | * phpcs:disable NeutronStandard.Functions.LongFunction.LongFunction 86 | */ 87 | public static function request(string $type): bool 88 | { 89 | // phpcs:disable Squiz.PHP.CommentedOutCode.Found 90 | switch ($type) { 91 | case 'installing': 92 | return \defined('WP_INSTALLING') && \WP_INSTALLING === true; 93 | case 'index': 94 | return \defined('WP_USE_THEMES') && \WP_USE_THEMES === true; 95 | case 'frontend': 96 | // Use !request('frontend') for admin pages. 97 | return (! \is_admin() || \wp_doing_ajax() ) && ! \wp_doing_cron(); 98 | case 'admin': 99 | // Includes admin-ajax :( 100 | return \is_admin(); 101 | case 'login': 102 | return isset($_SERVER['REQUEST_URI']) 103 | && \explode('?', $_SERVER['REQUEST_URI'])[0] 104 | === \wp_parse_url(\wp_login_url('', true), \PHP_URL_PATH); 105 | case 'async-upload': 106 | return isset($_SERVER['SCRIPT_FILENAME']) 107 | && \ABSPATH . 'wp-admin/async-upload.php' === $_SERVER['SCRIPT_FILENAME']; 108 | case 'preview': // in 'parse_query' action if (is_main_query()) 109 | return \is_preview() || \is_customize_preview(); 110 | case 'autosave': // After 'heartbeat_received', 500 action 111 | // Autosave post while editing and Heartbeat. 112 | return \defined('DOING_AUTOSAVE') && \DOING_AUTOSAVE === true; 113 | case 'rest': // After 'parse_request' action 114 | return \defined('REST_REQUEST') && \REST_REQUEST === true; 115 | case 'ajax': 116 | return \wp_doing_ajax(); 117 | case 'xmlrpc': 118 | return \defined('XMLRPC_REQUEST') && \XMLRPC_REQUEST === true; 119 | case 'trackback': // In 'parse_query' 120 | return \is_trackback(); 121 | case 'search': // In 'parse_query' 122 | return \is_search(); 123 | case 'feed': // In 'parse_query' 124 | return \is_feed(); 125 | case 'robots': // In 'parse_query' 126 | return \is_robots(); 127 | case 'cron': 128 | return \wp_doing_cron(); 129 | case 'wp-cli': 130 | return \defined('WP_CLI') && \WP_CLI === true; 131 | default: 132 | \_doing_it_wrong(__METHOD__, \esc_html(\sprintf('Unknown request type: %s', $type)), '0.1.0'); 133 | return false; 134 | } 135 | // phpcs:enable 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/SiteInfo.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | use WP_Filesystem_Base; 16 | 17 | use function trailingslashit; 18 | 19 | /** 20 | * Provide information on core paths and URLs. 21 | */ 22 | class SiteInfo 23 | { 24 | /** 25 | * Site info. 26 | * 27 | * @var array 28 | */ 29 | protected $info = []; 30 | 31 | /** 32 | * Set paths and URLs. 33 | * 34 | * @see https://codex.wordpress.org/Determining_Plugin_and_Content_Directories 35 | */ 36 | protected function init(): void 37 | { 38 | $uploadPathAndUrl = \wp_upload_dir(); 39 | // phpcs:disable NeutronStandard.AssignAlign.DisallowAssignAlign.Aligned 40 | $this->info = [ 41 | // Core 42 | 'site_path' => \ABSPATH, 43 | 'site_url' => \site_url(), 44 | 'home_path' => $this->getHomePath(), 45 | 'home_url' => \get_home_url(), 46 | 'includes_path' => \ABSPATH . \WPINC, 47 | 'includes_url' => \includes_url(), 48 | 49 | // Content 50 | 'content_path' => \WP_CONTENT_DIR, 51 | 'content_url' => \content_url(), 52 | 'uploads_path' => $uploadPathAndUrl['basedir'], 53 | 'uploads_url' => $uploadPathAndUrl['baseurl'], 54 | 55 | // Plugins 56 | 'plugins_path' => \WP_PLUGIN_DIR, 57 | 'plugins_url' => \plugins_url(), 58 | 'mu_plugins_path' => \WPMU_PLUGIN_DIR, 59 | 'mu_plugins_url' => \WPMU_PLUGIN_URL, 60 | 61 | // Themes 62 | 'themes_root_path' => \get_theme_root(), 63 | 'themes_root_url' => \get_theme_root_uri(), 64 | 'parent_theme_path' => \get_template_directory(), 65 | 'parent_theme_url' => \get_template_directory_uri(), 66 | 'child_theme_path' => \get_stylesheet_directory(), 67 | 'child_theme_url' => \get_stylesheet_directory_uri(), 68 | ]; 69 | // phpcs:enable 70 | } 71 | 72 | /** 73 | * Public API. 74 | */ 75 | public function getPath(string $name): string 76 | { 77 | return $this->getInfo($name, '_path'); 78 | } 79 | 80 | /** 81 | * Public API. 82 | */ 83 | public function getUrl(string $name): string 84 | { 85 | return $this->getInfo($name, '_url'); 86 | } 87 | 88 | /** 89 | * Public API. 90 | */ 91 | public function getUrlBasename(string $name): string 92 | { 93 | return \basename($this->getUrl($name)); 94 | } 95 | 96 | /** 97 | * Public API. 98 | */ 99 | public function usingChildTheme(): bool 100 | { 101 | $this->setInfo(); 102 | 103 | return trailingslashit($this->info['parent_theme_path']) !== trailingslashit($this->info['child_theme_path']); 104 | } 105 | 106 | /** 107 | * Public API. 108 | */ 109 | public function isUploadsWritable(): bool 110 | { 111 | // phpcs:disable Squiz.NamingConventions.ValidVariableName 112 | global $wp_filesystem; 113 | if (! $wp_filesystem instanceof WP_Filesystem_Base) { 114 | require_once ABSPATH . 'wp-admin/includes/file.php'; 115 | } 116 | 117 | $this->setInfo(); 118 | 119 | $uploadsDir = trailingslashit($this->info['uploads_path']); 120 | 121 | return $wp_filesystem->exists($uploadsDir) && $wp_filesystem->is_writable($uploadsDir); 122 | // phpcs:enable 123 | } 124 | 125 | protected function setInfo(): void 126 | { 127 | if ($this->info !== []) { 128 | return; 129 | } 130 | 131 | if (! \did_action('init')) { 132 | throw new \LogicException('SiteInfo must be used in "init" action or later.'); 133 | } 134 | 135 | $this->init(); 136 | } 137 | 138 | protected function getInfo(string $name, string $suffix): string 139 | { 140 | $this->setInfo(); 141 | 142 | $key = $name . $suffix; 143 | if (! \array_key_exists($key, $this->info)) { 144 | throw new \DomainException('Unknown SiteInfo key: ' . $key); 145 | } 146 | 147 | return trailingslashit($this->info[$key]); 148 | } 149 | 150 | protected function getHomePath(): string 151 | { 152 | $homeUrl = \set_url_scheme(\get_option('home'), 'http'); 153 | $siteUrl = \set_url_scheme(\get_option('siteurl'), 'http'); 154 | if ($homeUrl !== '' && \strcasecmp($homeUrl, $siteUrl) !== 0) { 155 | $pos = \strripos(\ABSPATH, trailingslashit(\str_ireplace($homeUrl, '', $siteUrl))); 156 | if ($pos !== false) { 157 | return \substr(\ABSPATH, 0, $pos); 158 | } 159 | } 160 | 161 | return \ABSPATH; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | 7 | * @license https://opensource.org/licenses/MIT MIT 8 | * @link https://github.com/szepeviktor/toolkit4wp 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Toolkit4WP; 14 | 15 | use Traversable; 16 | 17 | use function esc_attr; 18 | use function esc_html; 19 | use function esc_url; 20 | use function sanitize_key; 21 | 22 | /** 23 | * Create an HTML attribute string from an array. 24 | * 25 | * @param array $attrs HTML attributes. 26 | * @return string 27 | */ 28 | function tagAttrString(array $attrs = []): string 29 | { 30 | // Attributes. 31 | $attrString = ''; 32 | foreach ($attrs as $attrName => $attrValue) { 33 | $attrName = \strtolower($attrName); 34 | $attrName = \preg_replace('/[^a-z0-9-]/', '', $attrName); 35 | // Boolean Attributes. 36 | if ($attrValue === null) { 37 | $attrString .= \sprintf(' %s', $attrName); 38 | continue; 39 | } 40 | 41 | $attrString .= \sprintf( 42 | ' %s="%s"', 43 | $attrName, 44 | \in_array($attrName, ['href', 'src'], true) 45 | ? esc_url($attrValue) 46 | : esc_attr($attrValue) 47 | ); 48 | } 49 | 50 | return $attrString; 51 | } 52 | 53 | /** 54 | * Create an HTML element with pure PHP. 55 | * 56 | * @see https://www.w3.org/TR/html/syntax.html#void-elements 57 | * 58 | * @param string $name Tag name. 59 | * @param array $attrs HTML attributes. 60 | * @param string|\Traversable $content Raw HTML content. 61 | * @return string 62 | * @throws \Exception 63 | */ 64 | function tag(string $name = 'div', array $attrs = [], $content = ''): string 65 | { 66 | $voids = [ 67 | 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 68 | 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr', 69 | ]; 70 | 71 | $name = sanitize_key($name); 72 | if ($content instanceof Traversable) { 73 | $content = \implode(\iterator_to_array($content)); 74 | } 75 | 76 | // Void elements. 77 | $isVoid = \in_array($name, $voids, true); 78 | if ($isVoid && $content !== '') { 79 | throw new \Exception('Void HTML element with content.'); 80 | } 81 | 82 | $attrString = tagAttrString($attrs); 83 | 84 | // Element. 85 | if ($isVoid) { 86 | return \sprintf('<%s%s>', $name, $attrString); 87 | } 88 | 89 | return \sprintf('<%s%s>%s', $name, $attrString, $content, $name); 90 | } 91 | 92 | /** 93 | * Create an HTML list. 94 | * 95 | * @param string $name Parent tag name. 96 | * @param array $attrs HTML attributes of the parent. 97 | * @param array $childrenContent Raw HTML content of children. 98 | * @param string $childTagName Name of children tags. 99 | * @return string 100 | */ 101 | function tagList( 102 | string $name = 'ul', 103 | array $attrs = [], 104 | array $childrenContent = [], 105 | string $childTagName = 'li' 106 | ): string { 107 | $content = \array_map( 108 | static function (string $child) use ($childTagName): string { 109 | return \sprintf('<%s>%s', $childTagName, $child, $childTagName); 110 | }, 111 | $childrenContent 112 | ); 113 | 114 | return tag($name, $attrs, \implode('', $content)); 115 | } 116 | 117 | /** 118 | * Create a DIV element with classes. 119 | * 120 | * @param string $classes 121 | * @param string $htmlContent 122 | * @return string 123 | */ 124 | function tagDivClass(string $classes, string $htmlContent = ''): string 125 | { 126 | return tag('div', ['class' => $classes], $htmlContent); 127 | } 128 | 129 | /** 130 | * Create an H3 element with classes. 131 | * 132 | * @param string $classes 133 | * @param string $htmlContent 134 | * @return string 135 | */ 136 | function tagH3Class(string $classes, string $htmlContent = ''): string 137 | { 138 | return tag('h3', ['class' => $classes], $htmlContent); 139 | } 140 | 141 | /** 142 | * Create an HTML element from tag name and array of attributes. 143 | * 144 | * @param array{tag: string, attrs: array} $skeleton 145 | * @param string $htmlContent 146 | * @return string 147 | */ 148 | function tagFromSkeleton(array $skeleton, string $htmlContent = ''): string 149 | { 150 | return tag($skeleton['tag'], $skeleton['attrs'], $htmlContent); 151 | } 152 | 153 | /** 154 | * Create a select element. 155 | * 156 | * @param array $attrs HTML attributes of the select. 157 | * @param array $options Option elements value=>item. 158 | */ 159 | function tagSelect(array $attrs, array $options, string $currentValue = ''): string 160 | { 161 | $optionElements = \array_map( 162 | static function (string $value, string $item) use ($currentValue): string { 163 | return tag( 164 | 'option', 165 | \array_merge( 166 | ['value' => $value], 167 | $value === $currentValue ? ['selected' => null] : [] 168 | ), 169 | esc_html($item) 170 | ); 171 | }, 172 | \array_keys($options), 173 | $options 174 | ); 175 | 176 | return tag( 177 | 'select', 178 | $attrs, 179 | \implode('', $optionElements) 180 | ); 181 | } 182 | --------------------------------------------------------------------------------