first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,297 @@
<?php
class Loco_error_AdminNotices extends Loco_hooks_Hookable {
/**
* @var Loco_error_AdminNotices
*/
private static $singleton;
/**
* @var Loco_error_Exception[]
*/
private $errors = [];
/**
* Inline messages are handled by our own template views
* @var bool
*/
private $inline = false;
/**
* @return Loco_error_AdminNotices
*/
public static function get(){
self::$singleton or self::$singleton = new Loco_error_AdminNotices;
return self::$singleton;
}
/**
* Enable temporary buffering of PHP errors, reducing error reporting to debug level.
* Call restore_error_handler to stop capturing.
* @param int $level PHP error level bit mask, e.g. E_WARNING
* @return void
*/
public static function capture( $level ){
set_error_handler( [__CLASS__,'handle_error'], $level );
}
/**
* @internal
* @param int $errno
* @param string $errstr
*/
public static function handle_error( $errno, $errstr /*$errfile, $errline*/ ){
if( $errno & (E_ERROR|E_USER_ERROR) ){
return false;
}
$label = $errno & (E_WARNING|E_USER_WARNING) ? 'Warning' : 'Notice';
self::debug( '[PHP '.$label.'] '.$errstr );
return true;
}
/**
* @param Loco_error_Exception $error
* @return Loco_error_Exception
*/
public static function add( Loco_error_Exception $error ){
$notices = self::get();
// Skip repeated error messages in same stack
foreach( $notices->errors as $previous ){
if( $error->isIdentical($previous) ){
return $previous;
}
}
// if exception wasn't thrown we have to do some work to establish where it was invoked
if( __FILE__ === $error->getRealFile() ){
$error->setCallee(1);
}
// write error immediately under WP_CLI
if( 'cli' === PHP_SAPI && class_exists('WP_CLI',false) ){
$error->logCli();
return $error;
}
// else buffer notices for displaying when UI is ready
$notices->errors[] = $error;
// do late flush if we missed the boat
if( did_action('loco_admin_init') ){
$notices->on_loco_admin_init();
}
if( did_action('admin_notices') ){
$notices->on_admin_notices();
}
// Log message automatically if enabled
if( $error->loggable() ){
$error->log();
}
return $error;
}
/**
* Raise a success message
* @param string $message
* @return Loco_error_Exception
*/
public static function success( $message ){
$notice = new Loco_error_Success($message);
return self::add( $notice->setCallee(1) );
}
/**
* Raise a failure message
* @param string $message
* @return Loco_error_Exception
*/
public static function err( $message ){
$notice = new Loco_error_Exception($message);
return self::add( $notice->setCallee(1) );
}
/**
* Raise a warning message
* @param string $message
* @return Loco_error_Exception
*/
public static function warn( $message ){
$notice = new Loco_error_Warning($message);
return self::add( $notice->setCallee(1) );
}
/**
* Raise a generic info message
* @param string $message
* @return Loco_error_Exception
*/
public static function info( $message ){
$notice = new Loco_error_Notice($message);
return self::add( $notice->setCallee(1) );
}
/**
* Raise a debug notice, if debug is enabled
* @param string $message
* @return Loco_error_Debug
*/
public static function debug( $message ){
$notice = new Loco_error_Debug($message);
$notice->setCallee(1);
loco_debugging() and self::add( $notice );
return $notice;
}
/**
* Destroy and return buffer
* @return Loco_error_Exception[]
*/
public static function destroy(){
$notices = self::$singleton;
if( $notices instanceof Loco_error_AdminNotices ){
$buffer = $notices->errors;
$notices->errors = [];
self::$singleton = null;
return $buffer;
}
return [];
}
/**
* @codeCoverageIgnore
* @deprecated Since PHP 5.4 there is no need to cast array via calls to jsonSerialize
*/
public static function destroyAjax(){
$data = [];
foreach( self::destroy() as $notice ){
$data[] = $notice->jsonSerialize();
}
return $data;
}
/**
* @return void
*/
private function flushHtml(){
if( $this->errors ){
$htmls = [];
foreach( $this->errors as $error ){
$html = sprintf (
'<p><strong class="has-icon">%s:</strong> <span>%s</span></p>',
esc_html( $error->getTitle() ),
esc_html( $error->getMessage() )
);
$styles = [ 'notice', 'notice-'.$error->getType() ];
if( $this->inline ){
$styles[] = 'inline';
}
if( $links = $error->getLinks() ){
$styles[] = 'has-nav';
$html .= '<nav>'.implode( '<span> | </span>', $links ).'</nav>';
}
$htmls[] = '<div class="'.implode(' ',$styles).'">'.$html.'</div>';
}
$this->errors = [];
echo implode("\n", $htmls),"\n";
}
}
/**
* @return void
*/
private function flushCli(){
foreach( $this->errors as $e ){
$e->logCli();
}
$this->errors = [];
}
/**
* admin_notices action handler.
*/
public function on_admin_notices(){
if( ! $this->inline ){
$this->flushHtml();
}
}
/**
* loco_admin_notices callback.
* Unlike WordPress "admin_notices" this fires from within template layout at the point we want them, hence they are marked as "inline"
*/
public function on_loco_admin_notices(){
$this->inline = true;
$this->flushHtml();
}
/**
* loco_admin_init callback
* When we know a Loco admin controller will render the page we will control the point at which notices are printed
*/
public function on_loco_admin_init(){
$this->inline = true;
}
/**
* @internal
* Make sure we always see notices if hooks didn't fire
*/
public function __destruct(){
$this->inline = false;
$this->flush();
// handle situation where test case will have lost the buffer
if( $this->errors && 'cli' === PHP_SAPI ){
throw new RuntimeException('Notices not flushed before destruction');
}
}
/**
* @param int $level
* @return Loco_error_Exception[]
*/
public function filter( $level ){
$e = [];
foreach( $this->errors as $error ){
if( $error->getLevel() <= $level ){
$e[] = $error;
}
}
return $e;
}
/**
* @internal
*/
public function flush(){
if( class_exists('WP_CLI',false) ){
$this->flushCli();
}
else if( loco_doing_ajax() ){
$this->errors = [];
}
else if( 'cli' !== PHP_SAPI ){
$this->flushHtml();
}
// else probably in unit test and not properly handled, leave significant errors in buffer
else {
$this->errors = $this->filter( Loco_error_Exception::LEVEL_WARNING );
}
return $this;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* Developer notice
*/
class Loco_error_Debug extends Loco_error_Exception {
/**
* {@inheritdoc}
*/
public function getType(){
return 'debug';
}
/**
* {@inheritdoc}
*/
public function getTitle(){
return __('Debug','loco-translate');
}
/**
* {@inheritdoc}
*/
public function getLevel(){
return Loco_error_Exception::LEVEL_DEBUG;
}
/**
* {@inheritDoc}
*/
public function logCli(){
WP_CLI::debug( $this->getMessage(), 'loco' );
}
/**
* Log debugging message to file without raising admin notice
* @codeCoverageIgnore
*/
public static function trace( ...$args ){
$message = array_shift($args);
if( $args ){
$message = vsprintf($message,$args);
}
$debug = new Loco_error_Debug($message);
$debug->setCallee(1);
$debug->log();
}
}

View File

@@ -0,0 +1,270 @@
<?php
/**
* Generic exception that we know has come from the Loco plugin
*/
class Loco_error_Exception extends Exception implements JsonSerializable {
const LEVEL_ERROR = 0;
const LEVEL_WARNING = 1;
const LEVEL_DEBUG = 2;
const LEVEL_NOLOG = 3;
/**
* Links to help docs etc.. to show along side error message
* @var array
*/
private $links = [];
/**
* Override file in which exception was thrown
* @var string
*/
private $_file;
/**
* Override line number from where exception was thrown
* @var int
*/
private $_line;
/**
* Whether log file writing is enabled
* @var bool
*/
private $_log = true;
/**
* {@inheritdoc}
*/
public function __construct( $message = '', $code = 0, $previous = null ) {
parent::__construct( $message, $code, $previous );
}
/**
* @return Throwable
*/
private function getRootException(){
$current = $this;
// note that getPrevious is absent in PHP < 5.3
while( method_exists($current,'getPrevious') && ( $next = $current->getPrevious() ) ){
$current = $next;
}
return $current;
}
/**
* @return string
*/
public function getRealFile(){
if( $this->_file ){
return $this->_file;
}
return $this->getRootException()->getFile();
}
/**
* @return int
*/
public function getRealLine(){
if( $this->_line ){
return $this->_line;
}
return $this->getRootException()->getLine();
}
/**
* @return array
*/
public function getRealTrace(){
return $this->getRootException()->getTrace();
}
/**
* @param int $depth number of levels up from callee
*/
public function setCallee( int $depth = 0 ):self {
$stack = debug_backtrace(0);
$callee = $stack[$depth];
$this->_file = $callee['file'];
$this->_line = $callee['line'];
// TODO could also log the stack trace from $depth upwards, but not required unless being logged or thrown
return $this;
}
/**
* Write this error to file regardless of log level
* @return void
*/
public function log(){
$file = new Loco_fs_File( $this->getRealFile() );
$path = $file->getRelativePath( loco_plugin_root() );
$text = sprintf('[Loco.%s] "%s" in %s:%u', $this->getType(), $this->getMessage(), $path, $this->getRealLine() );
// separate error log for cli tests
if( 'cli' === PHP_SAPI && defined('LOCO_TEST_DATA_ROOT') ){
error_log( '['.date('c').'] '.$text."\n", 3, 'debug.log' );
//fwrite( STDERR, $this->getType().': '.$this->getMessage()."\n" );
}
// Else write to default PHP log, but note that WordPress may have set this to wp-content/debug.log.
// If no `error_log` is set this will send message to the SAPI, so check your httpd/fast-cgi errors too.
else {
error_log( $text, 0 );
}
}
/**
* Get view template for rendering error to HTML.
* @return string path relative to root tpl directory
*/
public function getTemplate(){
return 'admin/errors/generic';
}
/**
* Get notice level short code as a string
* @return string
*/
public function getType(){
return 'error';
}
/**
* Get verbosity level
* @return int
*/
public function getLevel(){
return self::LEVEL_ERROR;
}
/**
* Call wp cli logging function
* @return void
*/
public function logCli(){
WP_CLI::error( $this->getMessage(), false );
}
/**
* Get localized notice level name
* @return string
*/
public function getTitle(){
return __('Error','loco-translate');
}
/**
* @return array
*/
#[ReturnTypeWillChange]
public function jsonSerialize(){
$a = [
'code' => $this->getCode(),
'type' => $this->getType(),
'title' => $this->getTitle(),
'message' => $this->getMessage(),
];
/*if( loco_debugging() ){
$a['file'] = str_replace( ABSPATH, '', $this->getRealFile() );
$a['line'] = $this->getRealLine();
$a = self::recurseJsonSerialize($a,$this);
}*/
return $a;
}
/**
* @param string[] $a
* @return array modified from $a
* @codeCoverageIgnore
*/
private static function recurseJsonSerialize( array $a, Exception $child ){
$a['class'] = get_class($child);
$a['trace'] = $child->getTraceAsString();
$parent = $child->getPrevious();
if( $parent instanceof Exception ){
$a['previous'] = self::recurseJsonSerialize([],$parent);
}
return $a;
}
/**
* Push navigation links into error. Use for help pages etc..
* @param string $href
* @param string $text
* @return Loco_error_Exception
*/
public function addLink( $href, $text ){
$this->links[] = sprintf('<a href="%s">%s</a>', esc_url($href), esc_html($text) );
return $this;
}
/**
* @return array
*/
public function getLinks(){
return $this->links;
}
/**
* Convert generic exception to one of ours
* @param Exception $e original error
* @return Loco_error_Exception
*/
public static function convert( Exception $e ){
if( $e instanceof Loco_error_Exception ){
return $e;
}
return new Loco_error_Exception( $e->getMessage(), $e->getCode(), $e );
}
/**
* Test if this error should be automatically logged
* @return bool
*/
public function loggable(){
if( $this->_log ){
// Log messages of minimum priority and up, depending on debug mode
// note that non-debug level is in line with error_reporting set by WordPress (notices ignored)
$priority = loco_debugging() ? Loco_error_Exception::LEVEL_DEBUG : Loco_error_Exception::LEVEL_WARNING;
return $this->getLevel() <= $priority;
}
return false;
}
/**
* Suppress logging for this error. e.g if you want to warn in UI but don't want to pollute log files.
* @return self
*/
public function noLog(){
$this->_log = false;
return $this;
}
/**
* Check if passed exception is effectively the same as this one
* @return bool
*/
public function isIdentical( Exception $other ){
return $this->getCode() === $other->getCode()
&& $this->getMessage() === $other->getMessage()
&& $this->getType() === ( $other instanceof Loco_error_Exception ? $other->getType() : 0 );
}
}

View File

@@ -0,0 +1,7 @@
<?php
/**
*
*/
class Loco_error_LocaleException extends Loco_error_Exception {
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Generic, non-critical informational notice
* Not to be confused with an error notice. This is for onscreen messages, and won't be logged.
*/
class Loco_error_Notice extends Loco_error_Exception {
/**
* {@inheritdoc}
*/
public function getType(){
return 'info';
}
/**
* {@inheritdoc}
*/
public function getTitle(){
return __('Notice','loco-translate');
}
/**
* {@inheritdoc}
*/
public function getLevel(){
return Loco_error_Exception::LEVEL_NOLOG;
}
/**
* {@inheritDoc}
*/
public function logCli(){
WP_CLI::log( $this->getMessage() );
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Exception thrown when parsing fails
*/
class Loco_error_ParseException extends Loco_error_Exception {
/**
* @var string[]
*/
private $context;
/**
* @param int line number
* @param int column number
* @param string source in which to identify line and column
* @return self
*/
public function setContext( $line, $column, $source ){
$this->context = [];
$lines = preg_split( '/\\r?\\n/', $source, $line+1 );
$offset = $line - 1;
if( isset($lines[$offset]) ){
$this->context[] = $lines[$offset];
$this->context[] = str_repeat(' ', max(0,$column-1) ).'^';
}
$this->message = sprintf("Error at line %u, column %u: %s", $line, $column, $this->message );
return $this;
}
/**
* @param int zero-based offset to failure point
* @param string source in which to identify line and column
* @return self
*/
public function setOffsetContext( $offset, $source ){
$lines = preg_split( '/\\r?\\n/', substr($source,0,$offset) );
$line = count($lines);
$column = 1 + strlen( end($lines) );
return $this->setContext( $line, $column, $source );
}
/**
* @return string
*/
public function getContext(){
return is_array($this->context) ? implode("\n",$this->context) : '';
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Success message. Not really an exception obviously, but compatible with Loco_error_AdminNotices
*/
class Loco_error_Success extends Loco_error_Exception {
/**
* {@inheritdoc}
*/
public function getType(){
return 'success';
}
/**
* {@inheritdoc}
*/
public function getTitle(){
return __('OK','loco-translate');
}
/**
* {@inheritdoc}
*/
public function getLevel(){
return Loco_error_Exception::LEVEL_NOLOG;
}
/**
* {@inheritDoc}
*/
public function logCli(){
WP_CLI::success( $this->getMessage() );
}
}

View File

@@ -0,0 +1,8 @@
<?php
/**
* Exception type for failed uploads
*/
class Loco_error_UploadException extends Loco_error_Exception {
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Generic warning
*/
class Loco_error_Warning extends Loco_error_Exception {
/**
* {@inheritdoc}
*/
public function getType(){
return 'warning';
}
/**
* {@inheritdoc}
*/
public function getTitle(){
return __('Warning','loco-translate');
}
/**
* {@inheritdoc}
*/
public function getLevel(){
return Loco_error_Exception::LEVEL_WARNING;
}
/**
* {@inheritDoc}
*/
public function logCli(){
WP_CLI::warning( $this->getMessage() );
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* File system write error.
* Generally thrown from Loco_fs_FileWriter
*/
class Loco_error_WriteException extends Loco_error_Exception {
/**
* {@inheritdoc}
*/
public function getTitle(){
return __('Permission denied','loco-translate');
}
}

View File

@@ -0,0 +1,11 @@
<?php
/**
*
*/
class Loco_error_XmlParseException extends Loco_error_Exception {
public function getTitle(){
return __('XML parse error','loco-translate');
}
}