/home/vianto5/cititower.mx/wp-content/plugins/sitepress-multilingual-cms/classes/jobs
Edit: /home/vianto5/cititower.mx/wp-content/plugins/sitepress-multilingual-cms/classes/jobs/JobLog.php (15897B)
string, // REST or admin URL
* requestParams => array, // Parsed input params
* requestDateTime => string (ISO-8601),
* hasErrorLogs => bool,
* logsByGroup => [
* [
* groupId => int,
* label => string,
* logs => [
* [
* id => string, // Log message
* data => array, // Arbitrary payload
* trace => string[], // Call stack (file:line class/method)
* logType => int, // LOG_TYPE_*
* ...extra fields (apiCall, type, element_id, etc.)
* ],
* ...
* ],
* data => array // Group-level metadata
* ],
* ...
* ],
* logUid => string
* ]
*
* Case B: Admin "Send to translation" request
* - Same structure as Case A
* - requestParams may include posts, strings, batch info
* - logs may include st-batch element_id expansion (string_ids_in_batch)
*/
/**
* Option name that enables/disables logging.
* @var string
*/
const IS_ENABLED_OPTION_NAME = 'wpml_tm_job_log_is_enabled';
/**
* Log type: informational.
* @var int
*/
const LOG_TYPE_INFO = 0;
/**
* Log type: error.
* @var int
*/
const LOG_TYPE_ERROR = 1;
/**
* Group ID for sending jobs. Used when we send content for translation in the /inc/translation-management/translation-management.class.php.
* @var int
*/
const GROUP_ID_SEND_JOBS = 0;
/**
* Group ID for syncing jobs. Used when we sync jobs in the /classes/ATE/Sync/Process.php.
* @var int
*/
const GROUP_ID_SYNC_JOBS = 1;
/**
* Group ID for downloading jobs. Used when we download jobs in the /classes/ATE/Download/Process.php.
* @var int
*/
const GROUP_ID_DOWNLOAD_JOBS = 2;
/**
* Prevents calling from unit tests methods from $sitepress and $wpdb.
* @var bool
*/
private static $isInitialised = false;
/**
* Whether current request has any error logs. When it is setup request will render having error state in the logs UI.
* @var bool
*/
private static $hasRequestAnyErrorLog = false;
/**
* Cached flag whether logging is enabled. It is enabled by the button in the logging screen.
* That screen is accessible from the WPML support page.
*
* @var bool|null
*/
private static $isEnabled;
/**
* Stores current request URL. If this value is null that means request was never initialized.
* We want to log only explicit places in the code and avoid cases when we created some logs from some child class
* that called addLog out of the scope when we explicitly initialised the request. Such calls can create many
* useless entries in the database which give no value for the inspection of those logs and could make option entry grow fast.
* @var string|null
*/
private static $requestUrl;
/**
* Stores parsed request parameters from the current request.
* @var array
*/
private static $requestParams = [];
/**
* Request datetime in ISO-8601 UTC format.
* @var string|null
*/
private static $requestDateTime;
/**
* Logs grouped by logical groups. In Case A: REST / ATE sync/download request there can be 1 logical group.
*
* In Case B: Admin "Send to translation" request there can be multiple logical groups, 1 group per each send_jobs function call.
*
* This will allow to group logs into collapsible containers in the UI.
*
* @var array
*/
private static $logsByGroup = [];
/**
* Current opened group ID.
* @var int|null
*/
private static $groupId;
/**
* Current opened group label.
* @var string|null
*/
private static $groupLabel;
/**
* Logs of the current opened group.
* @var array
*/
private static $groupLogs = [];
/**
* Arbitrary data attached to current opened group.
* @var array
*/
private static $groupData = [];
/**
* Object IDs already logged to prevent recursion in dataToArray function call.
* @var array
*/
private static $alreadyAddedToLog = [];
/**
* Extra data merged into each log entry. The caller should manually call removeExtraLog data to remove it.
* Otherwise it will be added to all next logs. It is needed to track some optional properties inside each log
* inside current group, for example current processed language during the step when we send content for translation.
* @var array
*/
private static $extraLogData = [];
/**
* Collected string batch IDs. We need to store them to select strings later and show string names and ids from each batch in UI.
* @var array
*/
private static $stringBatchIds = [];
/**
* @param int $groupId
* @return bool
*/
public static function isSendJobsLogsGroup( $groupId ) {
return self::GROUP_ID_SEND_JOBS === (int) $groupId;
}
public static function init() {
self::$isInitialised = true;
add_action( 'shutdown', [ __CLASS__, 'shutdown' ], PHP_INT_MAX );
}
/**
* Checking in wp_options if logging was enabled from the UI by user.
*
* @return bool
*/
public static function isEnabled() {
if ( ! self::$isInitialised ) {
return false;
}
if ( ! is_null( self::$isEnabled ) ) {
return self::$isEnabled;
}
global $sitepress;
if ( ! $sitepress ) {
return false;
}
return self::$isEnabled = (bool) $sitepress->get_setting( self::IS_ENABLED_OPTION_NAME, false );
}
/**
* Checks whether logging is allowed for current request.
*
* @return bool
*/
private static function canLog() {
return self::wasRequestInitialised() && self::isEnabled();
}
/**
* @param bool $isEnabled
*/
public static function setIsEnabled( $isEnabled ) {
global $sitepress;
$sitepress->set_setting( self::IS_ENABLED_OPTION_NAME, (bool) $isEnabled );
$sitepress->save_settings();
}
/**
* This call should start the logging process and be called before starting any logging.
*
* @return void
*/
public static function maybeInitRequest() {
if ( ! is_null( self::$requestDateTime ) ) {
return;
}
self::$requestUrl = $_SERVER['REQUEST_URI'] ?? null;
self::$requestParams = self::getUrlParams();
self::$requestDateTime = gmdate('Y-m-d\TH:i:s\Z');
}
/**
* Check if request data was initialized.
*
* @return bool
*/
public static function wasRequestInitialised() {
return is_string( self::$requestUrl );
}
/**
* Start a new logging group.
*
* @param int $groupId
* @param string $groupLabel
* @param array $groupData
*/
public static function createNewGroup( $groupId, $groupLabel = '', $groupData = [] ) {
if ( ! self::canLog() ) {
return;
}
self::$groupId = $groupId;
self::$groupLabel = $groupLabel;
self::$groupLogs = [];
self::$groupData = $groupData;
}
public static function finishCurrentGroup() {
if ( ! self::canLog() ) {
return;
}
self::$logsByGroup[] = [
'groupId' => self::$groupId,
'label' => self::$groupLabel,
'logs' => self::$groupLogs,
'data' => self::$groupData,
];
self::$groupLogs = [];
self::$groupId = null;
self::$groupLabel = null;
self::$groupData = [];
}
/**
* Add extra metadata which will be auto appended to all next logs.
*
* @param string $key
* @param mixed $value
* @return void
*/
public static function addExtraLogData( $key, $value ) {
if ( ! self::canLog() ) {
return;
}
if (
$key === 'element_id'
&& isset( self::$extraLogData['type'] )
&& self::$extraLogData['type'] === 'st-batch'
) {
if ( ! in_array( $value, self::$stringBatchIds, true ) ) {
self::$stringBatchIds[] = $value;
}
}
self::$extraLogData[ $key ] = $value;
}
/**
* Remove extra metadata key.
*
* @param string $key
*/
public static function removeExtraLogData( $key ) {
if ( ! self::canLog() ) {
return;
}
unset( self::$extraLogData[ $key ] );
}
/**
* Add a log entry to the current log group.
*
* @param string|int $id
* @param mixed $data
* @param int $logType
*/
public static function add( $id, $data = [], $logType = self::LOG_TYPE_INFO ) {
if ( ! self::canLog() ) {
return;
}
self::$alreadyAddedToLog = [];
$logs = [
'id' => $id,
'data' => self::dataToArray( $data ),
'trace' => self::getTrace(),
'logType' => $logType,
];
$logs = array_merge( $logs, self::$extraLogData );
self::$groupLogs[] = $logs;
}
/**
* Check whether given log entry is an error.
*
* @return bool
*/
public static function isErrorLog( $log ) {
return (int) $log['logType'] === self::LOG_TYPE_ERROR;
}
/**
* Add an error log entry. Error log entries will be displayed in special way in the UX.
*
* @param string|int $id
* @param mixed $data
*/
public static function addError( $id, $data = [] ) {
self::$hasRequestAnyErrorLog = true;
self::add( $id, $data, self::LOG_TYPE_ERROR );
}
public static function getLogsCount() {
return FsJobLogStorage::getLogsCount();
}
/**
* Get all stored logs.
*
* @return array
*/
public static function getLogs() {
return FsJobLogStorage::getRequestLogs();
}
/**
* Clear all stored logs.
*
* @return bool
*/
public static function clearLogs() {
return FsJobLogStorage::clearAllLogs();
}
/**
* Shutdown handler – persists logs to DB.
*/
public static function shutdown() {
if ( ! self::canLog() ) {
return;
}
if ( count( self::$logsByGroup ) === 0 && count( self::$groupLogs ) === 0 ) {
return;
}
// Group did not closed properly because of the error or exception.
if ( count( self::$groupLogs ) > 0 ) {
self::finishCurrentGroup();
}
global $wpdb;
$requestParams = self::$requestParams;
if ( isset( $requestParams['posts'] ) && is_array( $requestParams['posts'] ) ) {
for ( $i = 0; $i < count( $requestParams['posts'] ); $i++ ) {
$postId = $requestParams['posts'][ $i ];
$post = get_post( $postId );
if ( is_object( $post ) ) {
$requestParams['posts'][ $i ] = $requestParams['posts'][ $i ] . ' (post title=' . $post->post_name . ')';
}
}
}
if ( isset( $requestParams['strings'] ) && is_array( $requestParams['strings'] ) ) {
$stringIds = array_map( 'intval', $requestParams['strings'] );
if ( $stringIds ) {
$placeholders = implode( ',', array_fill( 0, count( $stringIds ), '%d' ) );
$sql = "
SELECT id, value, context, gettext_context
FROM {$wpdb->prefix}icl_strings
WHERE id IN ($placeholders)
";
$results = $wpdb->get_results( $wpdb->prepare( $sql, $stringIds ), OBJECT_K );
foreach ( $requestParams['strings'] as $i => $stringId ) {
$id = (int) $stringId;
if ( isset( $results[ $id ] ) ) {
$value = $results[ $id ]->value;
$domain = $results[ $id ]->context;
$context = $results[ $id ]->gettext_context;
$requestParams['strings'][ $i ] = $id . ' (string value=`' . $value . '`, context=`' . $context . '`, domain=`' . $domain . '`)';
}
}
}
}
if ( count( self::$stringBatchIds ) > 0 ) {
$stringBatchIds = array_map( 'intval', self::$stringBatchIds );
if ( $stringBatchIds ) {
$placeholders = implode( ',', array_fill( 0, count( $stringBatchIds ), '%d' ) );
$sql = "
SELECT *
FROM {$wpdb->prefix}icl_string_batches
WHERE batch_id IN ($placeholders)
";
$results = $wpdb->get_results( $wpdb->prepare( $sql, $stringBatchIds ), ARRAY_A );
$stringIdsByBatchId = [];
foreach ( $results as $result ) {
$batchId = (int) $result['batch_id'];
$stringId = (int) $result['string_id'];
if ( ! isset( $stringIdsByBatchId[ $batchId ] ) ) {
$stringIdsByBatchId[ $batchId ] = [];
}
$stringIdsByBatchId[ $batchId ][] = $stringId;
}
// We need this info only for first log within same element_id group sequence.
$alreadyAddedBatches = [];
foreach ( self::$logsByGroup as &$groupLogs ) {
foreach ( $groupLogs['logs'] as &$log ) {
if ( ! isset( $log['type'] ) || $log['type'] !== 'st-batch' || ! isset( $log['element_id'] ) ) {
unset( $log );
continue;
}
$batchId = (int) $log['element_id'];
$stringIds = $stringIdsByBatchId[ $batchId ] ?? null;
if ( in_array( $batchId, $alreadyAddedBatches ) || ! is_array( $stringIds ) ) {
unset( $log );
continue;
}
$log['string_ids_in_batch'] = $stringIds;
unset( $log );
$alreadyAddedBatches[] = $batchId;
}
unset( $groupLogs );
}
}
}
$log = [
'requestUrl' => self::$requestUrl,
'requestParams' => $requestParams,
'requestDateTime' => self::$requestDateTime,
'hasErrorLogs' => self::$hasRequestAnyErrorLog,
'logsByGroup' => self::$logsByGroup,
'logUid' => uniqid(),
];
FsJobLogStorage::writeRequestLog( $log );
}
/**
* @param object $object
*
* @return string
*/
private static function getObjectId( $object ) {
return (string) spl_object_hash( $object );
}
const MAX_DEPTH = 10;
const MAX_STRING_LENGTH = 1000;
const MAX_ARRAY_ITEMS = 1000;
/**
* Normalize log data to array-safe format.
*
* @param mixed $data
* @param int $depth
*
* @return array
*/
private static function dataToArray( $data, $depth = 0 ) {
if ( $depth > self::MAX_DEPTH ) {
return '[DEPTH_LIMIT]';
}
if ( is_callable( $data ) ) {
return 'callable';
}
if ( is_resource( $data ) ) {
return 'resource';
}
if ( is_string( $data ) && strlen( $data ) > self::MAX_STRING_LENGTH ) {
return substr( $data, 0, self::MAX_STRING_LENGTH ) . '…[TRUNCATED]';
}
if ( is_object( $data ) ) {
$id = self::getObjectId( $data );
if ( in_array( $id, self::$alreadyAddedToLog ) ) {
return '[RECURSION]';
}
self::$alreadyAddedToLog[] = $id;
$data = get_object_vars( $data );
}
$count = 0;
if ( is_array( $data ) ) {
$result = [];
foreach ( $data as $key => $value ) {
if ( ++$count > self::MAX_ARRAY_ITEMS ) {
$result[ $key ] = '[ARRAY_TRUNCATED]';
break;
}
if ( $key === 'signedUrl' && is_string( $value ) && strpos( $value, 'signature=' ) !== false ) {
$result[ $key ] = preg_replace(
'/(?:^|[?&])(signature|token|shared_key)=([^&]+)/i',
'$1=[REMOVED]',
$value
);
continue;
}
$result[ $key ] = self::dataToArray( $value, $depth + 1 );
}
return $result;
}
return $data;
}
/**
* @param string $url
* @return string
*/
private static function sanitizeSignedUrlWithRegex( $url ) {
$res = preg_replace(
'/(?:^|[?&])(signature|token|shared_key)=([^&]+)/',
'$1=[REMOVED]',
$url
);
return is_string( $res ) ? $res : '[EMPTY]';
}
/**
* Parse JSON request body into array.
*
* @return array
*/
private static function getUrlParams() {
$body = file_get_contents('php://input');
if ($body === false || $body === '') {
return [];
}
$params = json_decode( $body, true );
return is_array($params) ? $params : [];
}
/**
* Get simplified backtrace for logging.
*
* @return array
*/
private static function getTrace() {
$debug_backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
$traces = [];
foreach ( $debug_backtrace as $trace ) {
$trace = [
'file' => $trace['file'] ?? '',
'line' => $trace['line'] ?? '',
'class' => $trace['class'] ?? '',
'type' => $trace['type'] ?? '',
'function' => $trace['function'] ?? '',
];
$traces[] = $trace['file'] . ':' . $trace['line'] . ' ' . $trace['class'] . '/' . $trace['function'];
}
return $traces;
}
}