Coverage Report

Created: 2026-09-23 07:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wt/src/Wt/WMessageResources.C
Line
Count
Source
1
/*
2
 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
3
 *
4
 * See the LICENSE file for terms of use.
5
 */
6
#ifndef WT_CNOR
7
#include <fstream>
8
#include <cstring>
9
10
#include "Wt/Exception/WInvalidFormatException.h"
11
#include "Wt/Exception/WInvalidOperationException.h"
12
13
#include "Wt/WLocale.h"
14
#include "Wt/WLogger.h"
15
#include "Wt/WMessageResources.h"
16
#include "Wt/WStringStream.h"
17
18
#include "DomElement.h"
19
#include "WebUtils.h"
20
21
#include "thirdparty/rapidxml/rapidxml.hpp"
22
#include "thirdparty/rapidxml/rapidxml_print.hpp"
23
24
using namespace Wt;
25
using namespace Wt::rapidxml;
26
27
#ifndef WT_NO_SPIRIT
28
29
#include <boost/version.hpp>
30
31
#if BOOST_VERSION < 103600
32
#include <boost/spirit.hpp>
33
#include <boost/spirit/phoenix/binders.hpp>
34
#else
35
#include <boost/spirit/include/classic_core.hpp>
36
#include <boost/spirit/include/classic_attribute.hpp>
37
#include <boost/spirit/include/phoenix1_binders.hpp>
38
#endif
39
40
namespace {
41
42
#if BOOST_VERSION < 103600
43
  using namespace boost::spirit;
44
#else
45
  using namespace boost::spirit::classic;
46
#endif
47
  using namespace boost;
48
49
struct CExpressionParser : grammar<CExpressionParser>
50
{
51
  struct ParseState
52
  {
53
    bool condition_;
54
  };
55
56
  CExpressionParser(::int64_t n, int &result, ParseState &state) :
57
0
    n_(n),
58
0
    result_(result),
59
0
    state_(state)
60
0
  {}
61
62
  struct value_closure : closure<value_closure, ::int64_t, ::int64_t>
63
  {
64
    member1 value;
65
    member2 condition;
66
  };
67
68
  template <typename ScannerT>
69
  struct definition
70
  {
71
    definition(CExpressionParser const& self)
72
0
    {
73
0
      using namespace boost::spirit;
74
0
      using namespace phoenix;
75
76
0
      group
77
0
        = '('
78
0
          >> expression[group.value = arg1]
79
0
          >> ')'
80
0
        ;
81
82
       // A statement can end at the end of the line, or with a semicolon.
83
0
      statement
84
0
        =   ( expression[bind(&CExpressionParser::set_result)(self, arg1)]
85
0
          )
86
0
        >> (end_p | ';')
87
0
        ;
88
89
0
      literal
90
0
        = uint_p[literal.value = arg1]
91
0
        ;
92
93
0
      factor
94
0
        = literal[factor.value = arg1]
95
0
        | group[factor.value = arg1]
96
0
        | ch_p('n')[factor.value = bind(&CExpressionParser::get_n)(self)]
97
0
        ;
98
99
0
      term
100
0
        = factor[term.value = arg1]
101
0
          >> *( ('*' >> factor[term.value *= arg1])
102
0
              | ('/' >> factor[bind(&CExpressionParser::safe_div)(self, term.value, arg1)])
103
0
              | ('%' >> factor[bind(&CExpressionParser::safe_rem)(self, term.value, arg1)])
104
0
            )
105
0
        ;
106
107
0
      additive_expression
108
0
        = term[additive_expression.value = arg1]
109
0
          >> *( ('+' >> term[additive_expression.value += arg1])
110
0
              | ('-' >> term[additive_expression.value -= arg1])
111
0
            )
112
0
        ;
113
114
0
      expression
115
0
        = or_expression[expression.value = arg1]
116
0
                       [expression.condition = arg1]
117
0
        >> !( '?'
118
0
              >> expression[bind(&CExpressionParser::set_cond)
119
0
                            (self, expression.condition)]
120
0
                           [bind(&CExpressionParser::ternary_op)
121
0
                            (self, expression.value, arg1)]
122
0
              >> ':'
123
0
              >> expression[bind(&CExpressionParser::set_not_cond)
124
0
                            (self, expression.condition)]
125
0
                           [bind(&CExpressionParser::ternary_op)
126
0
                            (self, expression.value, arg1)]
127
0
                            )
128
0
        ;
129
130
0
      or_expression
131
0
        = and_expression[or_expression.value = arg1]
132
0
          >> *( "||" >> and_expression[bind(&CExpressionParser::or_op)
133
0
                                       (self, or_expression.value, arg1)] )
134
0
        ;
135
136
0
      and_expression
137
0
        = eq_expression[and_expression.value = arg1]
138
0
          >> *( "&&" >> eq_expression[bind(&CExpressionParser::and_op)
139
0
                                       (self, and_expression.value, arg1)] )
140
0
        ;
141
142
0
      eq_expression
143
0
        = relational_expression[eq_expression.value = arg1]
144
0
          >> *( ("==" >> relational_expression[bind(&CExpressionParser::eq_op)
145
0
                                               (self,
146
0
                                                eq_expression.value,
147
0
                                                arg1)])
148
0
              | ("!=" >> relational_expression[bind(&CExpressionParser::neq_op)
149
0
                                               (self,
150
0
                                                eq_expression.value,
151
0
                                                arg1)])
152
0
            )
153
0
        ;
154
155
0
      relational_expression
156
0
        = additive_expression[relational_expression.value = arg1]
157
0
          >> *( (">" >> additive_expression[bind(&CExpressionParser::gt_op)
158
0
                                            (self,
159
0
                                             relational_expression.value,
160
0
                                             arg1)])
161
0
              | (">=" >> additive_expression[bind(&CExpressionParser::gte_op)
162
0
                                             (self,
163
0
                                              relational_expression.value,
164
0
                                              arg1)])
165
0
              | ("<" >> additive_expression[bind(&CExpressionParser::lt_op)
166
0
                                            (self,
167
0
                                             relational_expression.value,
168
0
                                             arg1)])
169
0
              | ("<=" >> additive_expression[bind(&CExpressionParser::lte_op)
170
0
                                             (self,
171
0
                                              relational_expression.value,
172
0
                                              arg1)])
173
0
            )
174
0
        ;
175
0
    }
176
177
    rule<ScannerT> const&
178
0
    start() const { return statement; }
179
180
    rule<ScannerT> statement;
181
    rule<ScannerT, value_closure::context_t> expression, factor,
182
      group, literal, term, additive_expression, or_expression, and_expression,
183
      eq_expression, relational_expression;
184
  };
185
186
private:
187
0
  ::int64_t get_n() const { return n_; }
188
189
0
  void eq_op(::int64_t &x, ::int64_t y) const { x = x == y; }
190
0
  void neq_op(::int64_t &x, ::int64_t y) const { x = x != y; }
191
192
0
  void lt_op(::int64_t &x, ::int64_t y) const { x = x < y;}
193
0
  void gt_op(::int64_t &x, ::int64_t y) const { x = x > y;}
194
0
  void lte_op(::int64_t &x, ::int64_t y) const { x = x <= y;}
195
0
  void gte_op(::int64_t &x, ::int64_t y) const { x = x >= y;}
196
197
  void ternary_op(::int64_t &result, ::int64_t y) const
198
0
  {
199
0
    if (state_.condition_)
200
0
      result = y;
201
0
  }
202
203
  void set_cond(::int64_t condition) const
204
0
  {
205
0
    state_.condition_ = condition;
206
0
  }
207
208
  void set_not_cond(::int64_t condition) const
209
0
  {
210
0
    state_.condition_ = !condition;
211
0
  }
212
213
0
  void or_op(::int64_t &x, ::int64_t y) const { x = x || y; }
214
0
  void and_op(::int64_t &x, ::int64_t y) const { x = x && y; }
215
216
  void safe_div(::int64_t& result, ::int64_t y) const
217
0
  {
218
0
    switch (y) {
219
0
    case 0:
220
0
      throw WInvalidOperationException("WMessageResources::evalPluralCase(): Cannot divide by 0");
221
0
    case -1:
222
0
      result = -result; // Avoid overflow error when result is int64_t minimum value.
223
0
      break;
224
0
    default:
225
0
      result = result / y;
226
0
    }
227
0
  }
228
229
  void safe_rem(::int64_t& result, ::int64_t y) const
230
0
  {
231
0
    switch (y) {
232
0
    case 0:
233
0
      throw WInvalidOperationException("WMessageResources::evalPluralCase(): Cannot divide by 0");
234
0
    case -1:
235
0
      result = 0; // Avoid overflow error when result is int64_t minimum value.
236
0
      break;
237
0
    default:
238
0
      result = result % y;
239
0
    }
240
0
  }
241
242
0
  void set_result(int result) const { result_ = result; }
243
private :
244
  ::int64_t n_;
245
  int &result_;
246
  ParseState &state_;
247
248
public :
249
0
  int result() { return result_; }
250
};
251
252
} // anonymous namespace
253
254
#endif //WT_NO_SPIRIT
255
256
namespace {
257
  char *copy_chars(const char *begin, const char *end, char *out)
258
0
  {
259
0
    while (begin != end)
260
0
      *out++ = *begin++;
261
0
    return out;
262
0
  }
263
264
  std::string readElementContent(xml_node<> *x_parent,
265
                                 std::unique_ptr<char[]>& buf)
266
0
  {
267
0
    char *ptr = buf.get();
268
269
0
    if (x_parent->type() == node_cdata) {
270
0
      return std::string(x_parent->value(), x_parent->value_size());
271
0
    } else {
272
0
      for (xml_node<> *x_child = x_parent->first_node();
273
0
           x_child; x_child = x_child->next_sibling()) {
274
0
        if (x_child->type() == node_cdata) {
275
0
          ptr = copy_chars(x_child->value(),
276
0
                           x_child->value() + x_child->value_size(), ptr);
277
0
        } else {
278
0
          Wt::Utils::fixSelfClosingTags(x_child);
279
0
          ptr = print(ptr, *x_child, print_no_indenting);
280
0
        }
281
0
      }
282
283
0
      return std::string(buf.get(), ptr - buf.get());
284
0
    }
285
0
  }
286
287
  int attributeValueToInt(xml_attribute<> *x_attribute)
288
0
  {
289
0
    return Utils::stoi(std::string(x_attribute->value(),
290
0
                                 x_attribute->value_size()));
291
0
  }
292
}
293
294
namespace Wt {
295
296
LOGGER("WMessageResources");
297
298
WMessageResources::WMessageResources(const std::string& path,
299
                                     bool loadInMemory)
300
0
  : loadInMemory_(loadInMemory),
301
0
    path_(path),
302
0
    builtin_(nullptr)
303
0
{ }
304
305
WMessageResources::WMessageResources(const char *builtin)
306
0
  : loadInMemory_(true),
307
0
    builtin_(builtin)
308
0
{
309
0
  std::istringstream s(builtin,  std::ios::in | std::ios::binary);
310
0
#ifdef WT_THREADED
311
0
  std::lock_guard<std::recursive_mutex> lock(resourceMutex_);
312
0
#endif
313
0
  readResourceStream(s, resources_[""], "<internal resource bundle>");
314
0
}
315
316
std::set<std::string> WMessageResources::keys(const WLocale& locale) const
317
0
{
318
0
#ifdef WT_THREADED
319
0
  std::lock_guard<std::recursive_mutex> lock(resourceMutex_);
320
0
#endif
321
  
322
0
  load(locale);
323
324
0
  std::set<std::string> keys;
325
326
0
  for (auto& r : resources_) {
327
0
    if (r.first == locale.name()) {
328
0
      for (auto& k : r.second.map_)
329
0
        keys.insert(k.first);
330
0
      break;
331
0
    }
332
0
  }
333
334
0
  return keys;
335
0
}
336
337
void WMessageResources::load(const WLocale& locale) const
338
0
{
339
0
  if (!path_.empty()) {
340
0
    Resource& target = resources_[locale.name()];
341
0
    std::string l = locale.name();
342
343
0
    target.map_.clear();
344
345
0
    for (;;) {
346
0
      if (readResourceFile(l, target))
347
0
        break;
348
349
      /* try a lesser specified variant */
350
0
      std::string::size_type i = l.rfind('-');
351
0
      if (i != std::string::npos)
352
0
        l.erase(i);
353
0
      else {
354
0
        if (locale.name().empty())
355
0
          LOG_ERROR("Could not load resource bundle: " << path_ << ".xml");
356
0
        break;
357
0
      }
358
0
    }
359
0
  }
360
0
}
361
362
void WMessageResources::hibernate()
363
0
{
364
0
  if (!loadInMemory_) {
365
0
#ifdef WT_THREADED
366
0
    std::lock_guard<std::recursive_mutex> lock(resourceMutex_);
367
0
#endif
368
0
    resources_.clear();
369
0
  }
370
0
}
371
372
LocalizedString WMessageResources::resolveKey(const WLocale& locale, const std::string& key)
373
  const
374
0
{
375
0
  LocalizedString result = resolve(locale.name(), key);
376
0
  if (result)
377
0
    return result;
378
379
0
  return resolve(std::string(), key);
380
0
}
381
382
LocalizedString WMessageResources::resolve(const std::string& locale, const std::string& key)
383
  const
384
0
{
385
0
#ifdef WT_THREADED
386
0
  std::lock_guard<std::recursive_mutex> lock(resourceMutex_);
387
0
#endif
388
  
389
0
  if (resources_.find(locale) == resources_.end())
390
0
    load(locale);
391
392
0
  const Resource& res = resources_[locale];
393
394
0
  KeyValuesMap::const_iterator j = res.map_.find(key);
395
0
  if (j != res.map_.end()) {
396
0
    if (j->second.size() > 1 )
397
0
      return LocalizedString{};
398
0
    return LocalizedString{j->second[0], TextFormat::XHTML};
399
0
  }
400
401
0
  return LocalizedString{};
402
0
}
403
404
std::string WMessageResources::findCase(const std::vector<std::string> &cases,
405
                                        std::string pluralExpression,
406
                                        ::uint64_t amount)
407
  const
408
0
{
409
#ifdef WT_NO_SPIRIT
410
  throw WException("WString::trn() requires the spirit library.");
411
#else
412
0
  int c = evalPluralCase(pluralExpression, amount);
413
414
0
  if (c > (int)cases.size() - 1 || c < 0) {
415
0
    WStringStream error;
416
0
    error << "Expression '" << pluralExpression << "' evaluates to '"
417
0
          << c << "' for n=" << std::to_string(amount);
418
419
0
    if (c < 0)
420
0
      error << " and values smaller than 0 are not allowed.";
421
0
    else
422
0
      error << " which is greater than the list of cases (size="
423
0
            << (int)cases.size() << ").";
424
425
0
    throw WException(error.c_str());
426
0
  }
427
428
0
  return cases[c];
429
0
#endif // WT_NO_SPIRIT
430
0
}
431
432
LocalizedString WMessageResources::resolvePluralKey(const WLocale& locale,
433
                                         const std::string& key,
434
                                         ::uint64_t amount) const
435
0
{
436
0
  LocalizedString result = resolvePlural(locale.name(), key, amount);
437
0
  if (result)
438
0
    return result;
439
440
0
  return resolvePlural(std::string(), key, amount);
441
0
}
442
443
LocalizedString WMessageResources::resolvePlural(const std::string& locale,
444
                                      const std::string& key,
445
                                      ::uint64_t amount) const
446
0
{
447
0
#ifdef WT_THREADED
448
0
  std::lock_guard<std::recursive_mutex> lock(resourceMutex_);
449
0
#endif
450
  
451
0
  if (resources_.find(locale) == resources_.end())
452
0
    load(locale);
453
454
0
  Resource& res = resources_[locale];
455
456
0
  KeyValuesMap::const_iterator j = res.map_.find(key);
457
0
  if (j != res.map_.end()) {
458
0
    if (j->second.size() != res.pluralCount_ )
459
0
      return LocalizedString{};
460
0
    std::string result = findCase(j->second, res.pluralExpression_, amount);
461
0
    return LocalizedString{result, TextFormat::XHTML};
462
0
  } else
463
0
    return LocalizedString{};
464
0
}
465
466
bool WMessageResources::readResourceFile(const std::string& locale,
467
                                         Resource& resource) const
468
0
{
469
0
  if (!path_.empty()) {
470
0
    std::string fileName
471
0
      = path_ + (locale.length() > 0 ? "_" : "") + locale + ".xml";
472
473
0
    std::ifstream s(fileName.c_str(), std::ios::binary);
474
0
    return readResourceStream(s, resource, fileName);
475
0
  } else {
476
0
    return false;
477
0
  }
478
0
}
479
480
bool WMessageResources::readResourceStream(std::istream &s,
481
                                           Resource& resource,
482
                                           const std::string &fileName) const
483
0
{
484
0
  if (!s)
485
0
    return false;
486
487
0
  s.seekg(0, std::ios::end);
488
0
  int length = s.tellg();
489
0
  s.seekg(0, std::ios::beg);
490
491
0
  enum { UTF8, UTF16LE, UTF16BE } encoding = UTF8;
492
493
  // See if we have UTF16 BOM
494
0
  if (length >= 2) {
495
0
    unsigned char m1, m2;
496
0
    m1 = s.get();
497
0
    m2 = s.get();
498
499
0
    if (m1 == 0xFE && m2 == 0xFF)
500
0
      encoding = UTF16BE;
501
0
    else if (m1 == 0xFF && m2 == 0xFE)
502
0
      encoding = UTF16LE;
503
0
    else {
504
0
      s.seekg(0, std::ios::beg);
505
0
    }
506
0
  }
507
508
0
  std::unique_ptr<char[]> text
509
0
    (new char[encoding == UTF8 ? length + 1 : (length-2)*2 + 1]);
510
511
0
  if (encoding != UTF8) {
512
    // Transcode from UTF16 stream to CharEncoding::UTF8 text
513
0
    const int BUFSIZE = 2048;
514
0
    unsigned char buf[BUFSIZE];
515
516
0
    unsigned long firstWord = 0;
517
0
    char *out = text.get();
518
519
0
    for (;;) {
520
0
      s.read((char *)buf, BUFSIZE);
521
0
      int read = s.gcount();
522
523
0
      for (int i = 0; i < read; i += 2) {
524
0
        unsigned long ch;
525
526
        // read next 2-byte char
527
0
        if (encoding == UTF16LE) {
528
0
          ch = buf[i+1];
529
0
          ch = (ch << 8) | buf[i];
530
0
        } else {
531
0
          ch = buf[i];
532
0
          ch = (ch << 8) | buf[i+1];
533
0
        }
534
535
0
        if (firstWord) {
536
          // second word of multi-word
537
0
          if (ch < 0xDC00 || ch > 0xDFFF) {
538
0
            read = 0;
539
0
            break;
540
0
          }
541
542
0
          unsigned long cp = 0x10000 + (((firstWord & 0x3FF) << 10)
543
0
                                        | (ch & 0x3FF));
544
545
0
          Wt::rapidxml::xml_document<>::insert_coded_character<0>(out, cp);
546
547
0
          firstWord = 0;
548
0
        } else if (ch >= 0xD800 && ch <= 0xDBFF) {
549
          // first word of multi-word
550
0
          firstWord = ch;
551
0
        } else {
552
          // single-word
553
0
          Wt::rapidxml::xml_document<>::insert_coded_character<0>(out, ch);
554
555
0
          firstWord = 0;
556
0
        }
557
0
      }
558
559
0
      if (read != BUFSIZE)
560
0
        break;
561
0
    }
562
563
0
    length = out - text.get();
564
0
  } else {
565
0
    s.read(text.get(), length);
566
0
  }
567
568
0
  text[length] = 0;
569
570
0
  try {
571
0
    xml_document<> doc;
572
0
    doc.parse<parse_no_string_terminators
573
0
      | parse_comment_nodes
574
0
      | parse_xhtml_entity_translation
575
0
      | parse_validate_closing_tags>(text.get());
576
577
0
    xml_node<> *x_root = doc.first_node("messages");
578
0
    if (!x_root)
579
0
      throw parse_error("Expected <messages> root element", text.get());
580
581
0
    xml_attribute<> *x_nplurals = x_root->first_attribute("nplurals");
582
0
    xml_attribute<> *x_plural = x_root->first_attribute("plural");
583
0
    if (x_nplurals && !x_plural)
584
0
      throw parse_error("Expected 'plural' attribute in <messages>",
585
0
                        x_root->value());
586
0
    if (x_plural && !x_nplurals)
587
0
      throw parse_error("Expected 'nplurals' attribute in <messages>",
588
0
                        x_root->value());
589
0
    if (x_nplurals && x_plural) {
590
0
      resource.pluralCount_ = attributeValueToInt(x_nplurals);
591
0
      resource.pluralExpression_
592
0
        = std::string(x_plural->value(), x_plural->value_size());
593
0
    } else {
594
0
      resource.pluralCount_ = 0;
595
0
    }
596
597
    // factor 2 in case we expanded <span/> to <span></span>
598
0
    std::unique_ptr<char[]> buf(new char[length * 2]);
599
600
0
    for (xml_node<> *x_message = x_root->first_node("message");
601
0
         x_message; x_message = x_message->next_sibling("message")) {
602
0
      xml_attribute<> *x_id = x_message->first_attribute("id");
603
0
      if (!x_id)
604
0
        throw parse_error("Missing message id", x_message->value());
605
606
0
      std::string id(x_id->value(), x_id->value_size());
607
608
0
      xml_node<> *x_plural = x_message->first_node("plural");
609
0
      if (x_plural) {
610
0
        if (resource.pluralCount_ == 0)
611
0
          throw parse_error("Expected 'nplurals' attribute in <message>",
612
0
                            x_plural->value());
613
614
0
        resource.map_[id] = std::vector<std::string>();
615
0
        resource.map_[id].reserve(resource.pluralCount_);
616
617
0
        std::vector<bool> visited;
618
0
        visited.reserve(resource.pluralCount_);
619
620
0
        for (unsigned i = 0; i < resource.pluralCount_; i++) {
621
0
          resource.map_[id].push_back(std::string());
622
0
          visited.push_back(false);
623
0
        }
624
625
0
        for (; x_plural; x_plural = x_plural->next_sibling("plural")) {
626
0
          xml_attribute<> *x_case = x_plural->first_attribute("case");
627
0
          int c = attributeValueToInt(x_case);
628
0
          if (c >= (int)resource.pluralCount_)
629
0
            throw parse_error("The attribute 'case' used in <plural> is greater"
630
0
                              " than the nplurals <messages> attribute.",
631
0
                              x_plural->value());
632
0
          visited[c] = true;
633
0
          resource.map_[id][c] = readElementContent(x_plural, buf);
634
0
        }
635
636
0
        for (unsigned i = 0; i < resource.pluralCount_; i++)
637
0
          if (!visited[i])
638
0
            throw parse_error("Missing plural case in <message>",
639
0
                              x_message->value());
640
0
      } else {
641
0
        resource.map_[id] = std::vector<std::string>();
642
0
        resource.map_[id].reserve(1);
643
0
        resource.map_[id].push_back(readElementContent(x_message, buf));
644
0
      }
645
0
    }
646
0
  } catch (parse_error& e) {
647
0
    LOG_ERROR("Error reading " << fileName
648
0
              << ": at character " << (int)(e.where<char>() - text.get())
649
0
              << ": " << e.what());
650
0
  }
651
652
0
  return true;
653
0
}
654
655
int WMessageResources::evalPluralCase(const std::string &expression,
656
                                      ::uint64_t n)
657
0
{
658
0
  int result = 0;
659
660
0
#ifndef WT_NO_SPIRIT
661
0
  CExpressionParser::ParseState state;
662
0
  CExpressionParser p(n, result, state);
663
0
  std::string tmp = expression;
664
0
  if (tmp.length() > 1000) {
665
    // This is a temporary catch to #12374
666
0
    throw Wt::WInvalidFormatException("WMessageResources::evalPluralCase(): The input it too long");
667
0
  }
668
0
  auto v = parse(tmp.begin(), tmp.end(), p, space_p);
669
0
  if (!v.full) {
670
0
    throw Wt::WInvalidFormatException("WMessageResources::evalPluralCase(): The parser encountered an invalid format");
671
0
  }
672
0
#endif // WT_NO_SPIRIT
673
674
0
  return result;
675
0
}
676
677
}
678
#endif // WT_CNOR
679