Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 258 |
PoStreamReader | |
0.00% |
0 / 1 |
|
0.00% |
0 / 16 |
5402 | |
0.00% |
0 / 258 |
getLangcode | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
setLangcode | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
getHeader | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
setHeader | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getURI | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
setURI | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
open | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
close | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 7 |
|||
readItem | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 6 |
|||
setSeek | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
getSeek | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
readHeader | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
readLine | |
0.00% |
0 / 1 |
2070 | |
0.00% |
0 / 168 |
|||
setItemFromArray | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 19 |
|||
parseQuoted | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 15 |
|||
shortenComments | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 12 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Component\Gettext\PoStreamReader. | |
*/ | |
namespace Drupal\Component\Gettext; | |
use Drupal\Component\Utility\SafeMarkup; | |
/** | |
* Implements Gettext PO stream reader. | |
* | |
* The PO file format parsing is implemented according to the documentation at | |
* http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files | |
*/ | |
class PoStreamReader implements PoStreamInterface, PoReaderInterface { | |
/** | |
* Source line number of the stream being parsed. | |
* | |
* @var int | |
*/ | |
private $_line_number = 0; | |
/** | |
* Parser context for the stream reader state machine. | |
* | |
* Possible contexts are: | |
* - 'COMMENT' (#) | |
* - 'MSGID' (msgid) | |
* - 'MSGID_PLURAL' (msgid_plural) | |
* - 'MSGCTXT' (msgctxt) | |
* - 'MSGSTR' (msgstr or msgstr[]) | |
* - 'MSGSTR_ARR' (msgstr_arg) | |
* | |
* @var string | |
*/ | |
private $_context = 'COMMENT'; | |
/** | |
* Current entry being read. Incomplete. | |
* | |
* @var array | |
*/ | |
private $_current_item = array(); | |
/** | |
* Current plural index for plural translations. | |
* | |
* @var int | |
*/ | |
private $_current_plural_index = 0; | |
/** | |
* URI of the PO stream that is being read. | |
* | |
* @var string | |
*/ | |
private $_uri = ''; | |
/** | |
* Language code for the PO stream being read. | |
* | |
* @var string | |
*/ | |
private $_langcode = NULL; | |
/** | |
* File handle of the current PO stream. | |
* | |
* @var resource | |
*/ | |
private $_fd; | |
/** | |
* The PO stream header. | |
* | |
* @var \Drupal\Component\Gettext\PoHeader | |
*/ | |
private $_header; | |
/** | |
* Object wrapper for the last read source/translation pair. | |
* | |
* @var \Drupal\Component\Gettext\PoItem | |
*/ | |
private $_last_item; | |
/** | |
* Indicator of whether the stream reading is finished. | |
* | |
* @var bool | |
*/ | |
private $_finished; | |
/** | |
* Array of translated error strings recorded on reading this stream so far. | |
* | |
* @var array | |
*/ | |
private $_errors; | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getLangcode() { | |
return $this->_langcode; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setLangcode($langcode) { | |
$this->_langcode = $langcode; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getHeader() { | |
return $this->_header; | |
} | |
/** | |
* Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader(). | |
* | |
* Not applicable to stream reading and therefore not implemented. | |
*/ | |
public function setHeader(PoHeader $header) { | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getURI() { | |
return $this->_uri; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setURI($uri) { | |
$this->_uri = $uri; | |
} | |
/** | |
* Implements Drupal\Component\Gettext\PoStreamInterface::open(). | |
* | |
* Opens the stream and reads the header. The stream is ready for reading | |
* items after. | |
* | |
* @throws Exception | |
* If the URI is not yet set. | |
*/ | |
public function open() { | |
if (!empty($this->_uri)) { | |
$this->_fd = fopen($this->_uri, 'rb'); | |
$this->readHeader(); | |
} | |
else { | |
throw new \Exception('Cannot open stream without URI set.'); | |
} | |
} | |
/** | |
* Implements Drupal\Component\Gettext\PoStreamInterface::close(). | |
* | |
* @throws Exception | |
* If the stream is not open. | |
*/ | |
public function close() { | |
if ($this->_fd) { | |
fclose($this->_fd); | |
} | |
else { | |
throw new \Exception('Cannot close stream that is not open.'); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function readItem() { | |
// Clear out the last item. | |
$this->_last_item = NULL; | |
// Read until finished with the stream or a complete item was identified. | |
while (!$this->_finished && is_null($this->_last_item)) { | |
$this->readLine(); | |
} | |
return $this->_last_item; | |
} | |
/** | |
* Sets the seek position for the current PO stream. | |
* | |
* @param int $seek | |
* The new seek position to set. | |
*/ | |
public function setSeek($seek) { | |
fseek($this->_fd, $seek); | |
} | |
/** | |
* Gets the pointer position of the current PO stream. | |
*/ | |
public function getSeek() { | |
return ftell($this->_fd); | |
} | |
/** | |
* Read the header from the PO stream. | |
* | |
* The header is a special case PoItem, using the empty string as source and | |
* key-value pairs as translation. We just reuse the item reader logic to | |
* read the header. | |
*/ | |
private function readHeader() { | |
$item = $this->readItem(); | |
// Handle the case properly when the .po file is empty (0 bytes). | |
if (!$item) { | |
return; | |
} | |
$header = new PoHeader; | |
$header->setFromString(trim($item->getTranslation())); | |
$this->_header = $header; | |
} | |
/** | |
* Reads a line from the PO stream and stores data internally. | |
* | |
* Expands $this->_current_item based on new data for the current item. If | |
* this line ends the current item, it is saved with setItemFromArray() with | |
* data from $this->_current_item. | |
* | |
* An internal state machine is maintained in this reader using | |
* $this->_context as the reading state. PO items are in between COMMENT | |
* states (when items have at least one line or comment in between them) or | |
* indicated by MSGSTR or MSGSTR_ARR followed immediately by an MSGID or | |
* MSGCTXT (when items closely follow each other). | |
* | |
* @return | |
* FALSE if an error was logged, NULL otherwise. The errors are considered | |
* non-blocking, so reading can continue, while the errors are collected | |
* for later presentation. | |
*/ | |
private function readLine() { | |
// Read a line and set the stream finished indicator if it was not | |
// possible anymore. | |
$line = fgets($this->_fd); | |
$this->_finished = ($line === FALSE); | |
if (!$this->_finished) { | |
if ($this->_line_number == 0) { | |
// The first line might come with a UTF-8 BOM, which should be removed. | |
$line = str_replace("\xEF\xBB\xBF", '', $line); | |
// Current plurality for 'msgstr[]'. | |
$this->_current_plural_index = 0; | |
} | |
// Track the line number for error reporting. | |
$this->_line_number++; | |
// Initialize common values for error logging. | |
$log_vars = array( | |
'%uri' => $this->getURI(), | |
'%line' => $this->_line_number, | |
); | |
// Trim away the linefeed. \\n might appear at the end of the string if | |
// another line continuing the same string follows. We can remove that. | |
$line = trim(strtr($line, array("\\\n" => ""))); | |
if (!strncmp('#', $line, 1)) { | |
// Lines starting with '#' are comments. | |
if ($this->_context == 'COMMENT') { | |
// Already in comment context, add to current comment. | |
$this->_current_item['#'][] = substr($line, 1); | |
} | |
elseif (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { | |
// We are currently in string context, save current item. | |
$this->setItemFromArray($this->_current_item); | |
// Start a new entry for the comment. | |
$this->_current_item = array(); | |
$this->_current_item['#'][] = substr($line, 1); | |
$this->_context = 'COMMENT'; | |
return; | |
} | |
else { | |
// A comment following any other context is a syntax error. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars); | |
return FALSE; | |
} | |
return; | |
} | |
elseif (!strncmp('msgid_plural', $line, 12)) { | |
// A plural form for the current source string. | |
if ($this->_context != 'MSGID') { | |
// A plural form can only be added to an msgid directly. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Remove 'msgid_plural' and trim away whitespace. | |
$line = trim(substr($line, 12)); | |
// Only the plural source string is left, parse it. | |
$quoted = $this->parseQuoted($line); | |
if ($quoted === FALSE) { | |
// The plural form must be wrapped in quotes. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains a syntax error on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Append the plural source to the current entry. | |
if (is_string($this->_current_item['msgid'])) { | |
// The first value was stored as string. Now we know the context is | |
// plural, it is converted to array. | |
$this->_current_item['msgid'] = array($this->_current_item['msgid']); | |
} | |
$this->_current_item['msgid'][] = $quoted; | |
$this->_context = 'MSGID_PLURAL'; | |
return; | |
} | |
elseif (!strncmp('msgid', $line, 5)) { | |
// Starting a new message. | |
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { | |
// We are currently in string context, save current item. | |
$this->setItemFromArray($this->_current_item); | |
// Start a new context for the msgid. | |
$this->_current_item = array(); | |
} | |
elseif ($this->_context == 'MSGID') { | |
// We are currently already in the context, meaning we passed an id with no data. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Remove 'msgid' and trim away whitespace. | |
$line = trim(substr($line, 5)); | |
// Only the message id string is left, parse it. | |
$quoted = $this->parseQuoted($line); | |
if ($quoted === FALSE) { | |
// The message id must be wrapped in quotes. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars); | |
return FALSE; | |
} | |
$this->_current_item['msgid'] = $quoted; | |
$this->_context = 'MSGID'; | |
return; | |
} | |
elseif (!strncmp('msgctxt', $line, 7)) { | |
// Starting a new context. | |
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { | |
// We are currently in string context, save current item. | |
$this->setItemFromArray($this->_current_item); | |
$this->_current_item = array(); | |
} | |
elseif (!empty($this->_current_item['msgctxt'])) { | |
// A context cannot apply to another context. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Remove 'msgctxt' and trim away whitespaces. | |
$line = trim(substr($line, 7)); | |
// Only the msgctxt string is left, parse it. | |
$quoted = $this->parseQuoted($line); | |
if ($quoted === FALSE) { | |
// The context string must be quoted. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars); | |
return FALSE; | |
} | |
$this->_current_item['msgctxt'] = $quoted; | |
$this->_context = 'MSGCTXT'; | |
return; | |
} | |
elseif (!strncmp('msgstr[', $line, 7)) { | |
// A message string for a specific plurality. | |
if (($this->_context != 'MSGID') && | |
($this->_context != 'MSGCTXT') && | |
($this->_context != 'MSGID_PLURAL') && | |
($this->_context != 'MSGSTR_ARR')) { | |
// Plural message strings must come after msgid, msgxtxt, | |
// msgid_plural, or other msgstr[] entries. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Ensure the plurality is terminated. | |
if (strpos($line, ']') === FALSE) { | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Extract the plurality. | |
$frombracket = strstr($line, '['); | |
$this->_current_plural_index = substr($frombracket, 1, strpos($frombracket, ']') - 1); | |
// Skip to the next whitespace and trim away any further whitespace, | |
// bringing $line to the message text only. | |
$line = trim(strstr($line, " ")); | |
$quoted = $this->parseQuoted($line); | |
if ($quoted === FALSE) { | |
// The string must be quoted. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars); | |
return FALSE; | |
} | |
if (!isset($this->_current_item['msgstr']) || !is_array($this->_current_item['msgstr'])) { | |
$this->_current_item['msgstr'] = array(); | |
} | |
$this->_current_item['msgstr'][$this->_current_plural_index] = $quoted; | |
$this->_context = 'MSGSTR_ARR'; | |
return; | |
} | |
elseif (!strncmp("msgstr", $line, 6)) { | |
// A string pair for an msgid (with optional context). | |
if (($this->_context != 'MSGID') && ($this->_context != 'MSGCTXT')) { | |
// Strings are only valid within an id or context scope. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Remove 'msgstr' and trim away away whitespaces. | |
$line = trim(substr($line, 6)); | |
// Only the msgstr string is left, parse it. | |
$quoted = $this->parseQuoted($line); | |
if ($quoted === FALSE) { | |
// The string must be quoted. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars); | |
return FALSE; | |
} | |
$this->_current_item['msgstr'] = $quoted; | |
$this->_context = 'MSGSTR'; | |
return; | |
} | |
elseif ($line != '') { | |
// Anything that is not a token may be a continuation of a previous token. | |
$quoted = $this->parseQuoted($line); | |
if ($quoted === FALSE) { | |
// This string must be quoted. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars); | |
return FALSE; | |
} | |
// Append the string to the current item. | |
if (($this->_context == 'MSGID') || ($this->_context == 'MSGID_PLURAL')) { | |
if (is_array($this->_current_item['msgid'])) { | |
// Add string to last array element for plural sources. | |
$last_index = count($this->_current_item['msgid']) - 1; | |
$this->_current_item['msgid'][$last_index] .= $quoted; | |
} | |
else { | |
// Singular source, just append the string. | |
$this->_current_item['msgid'] .= $quoted; | |
} | |
} | |
elseif ($this->_context == 'MSGCTXT') { | |
// Multiline context name. | |
$this->_current_item['msgctxt'] .= $quoted; | |
} | |
elseif ($this->_context == 'MSGSTR') { | |
// Multiline translation string. | |
$this->_current_item['msgstr'] .= $quoted; | |
} | |
elseif ($this->_context == 'MSGSTR_ARR') { | |
// Multiline plural translation string. | |
$this->_current_item['msgstr'][$this->_current_plural_index] .= $quoted; | |
} | |
else { | |
// No valid context to append to. | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars); | |
return FALSE; | |
} | |
return; | |
} | |
} | |
// Empty line read or EOF of PO stream, close out the last entry. | |
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { | |
$this->setItemFromArray($this->_current_item); | |
$this->_current_item = array(); | |
} | |
elseif ($this->_context != 'COMMENT') { | |
$this->_errors[] = SafeMarkup::format('The translation stream %uri ended unexpectedly at line %line.', $log_vars); | |
return FALSE; | |
} | |
} | |
/** | |
* Store the parsed values as a PoItem object. | |
*/ | |
public function setItemFromArray($value) { | |
$plural = FALSE; | |
$comments = ''; | |
if (isset($value['#'])) { | |
$comments = $this->shortenComments($value['#']); | |
} | |
if (is_array($value['msgstr'])) { | |
// Sort plural variants by their form index. | |
ksort($value['msgstr']); | |
$plural = TRUE; | |
} | |
$item = new PoItem(); | |
$item->setContext(isset($value['msgctxt']) ? $value['msgctxt'] : ''); | |
$item->setSource($value['msgid']); | |
$item->setTranslation($value['msgstr']); | |
$item->setPlural($plural); | |
$item->setComment($comments); | |
$item->setLangcode($this->_langcode); | |
$this->_last_item = $item; | |
$this->_context = 'COMMENT'; | |
} | |
/** | |
* Parses a string in quotes. | |
* | |
* @param $string | |
* A string specified with enclosing quotes. | |
* | |
* @return | |
* The string parsed from inside the quotes. | |
*/ | |
function parseQuoted($string) { | |
if (substr($string, 0, 1) != substr($string, -1, 1)) { | |
// Start and end quotes must be the same. | |
return FALSE; | |
} | |
$quote = substr($string, 0, 1); | |
$string = substr($string, 1, -1); | |
if ($quote == '"') { | |
// Double quotes: strip slashes. | |
return stripcslashes($string); | |
} | |
elseif ($quote == "'") { | |
// Simple quote: return as-is. | |
return $string; | |
} | |
else { | |
// Unrecognized quote. | |
return FALSE; | |
} | |
} | |
/** | |
* Generates a short, one-string version of the passed comment array. | |
* | |
* @param $comment | |
* An array of strings containing a comment. | |
* | |
* @return | |
* Short one-string version of the comment. | |
*/ | |
private function shortenComments($comment) { | |
$comm = ''; | |
while (count($comment)) { | |
$test = $comm . substr(array_shift($comment), 1) . ', '; | |
if (strlen($comm) < 130) { | |
$comm = $test; | |
} | |
else { | |
break; | |
} | |
} | |
return trim(substr($comm, 0, -2)); | |
} | |
} |