root/lib/common/schemas.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. xml_log
  2. xml_latest_schema_index
  3. xml_minimum_schema_index
  4. xml_latest_schema
  5. version_from_filename
  6. schema_filter
  7. schema_sort
  8. add_schema
  9. add_schema_by_version
  10. wrap_libxslt
  11. crm_schema_init
  12. relaxng_invalid_stderr
  13. validate_with_relaxng
  14. crm_schema_cleanup
  15. validate_with
  16. validate_with_silent
  17. dump_file
  18. validate_xml_verbose
  19. validate_xml
  20. cib_upgrade_err
  21. apply_transformation
  22. apply_upgrade
  23. get_schema_name
  24. get_schema_version
  25. update_validation
  26. cli_config_update

   1 /*
   2  * Copyright 2004-2020 the Pacemaker project contributors
   3  *
   4  * The version control history for this file may have further details.
   5  *
   6  * This source code is licensed under the GNU Lesser General Public License
   7  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
   8  */
   9 
  10 #include <crm_internal.h>
  11 
  12 #include <stdio.h>
  13 #include <string.h>
  14 #include <dirent.h>
  15 #include <errno.h>
  16 #include <sys/stat.h>
  17 #include <stdarg.h>
  18 
  19 #include <libxml/relaxng.h>
  20 
  21 #if HAVE_LIBXSLT
  22 #  include <libxslt/xslt.h>
  23 #  include <libxslt/transform.h>
  24 #  include <libxslt/security.h>
  25 #  include <libxslt/xsltutils.h>
  26 #endif
  27 
  28 #include <crm/msg_xml.h>
  29 #include <crm/common/xml.h>
  30 #include <crm/common/xml_internal.h>  /* PCMK__XML_LOG_BASE */
  31 
  32 typedef struct {
  33     unsigned char v[2];
  34 } schema_version_t;
  35 
  36 #define SCHEMA_ZERO { .v = { 0, 0 } }
  37 
  38 #define schema_scanf(s, prefix, version, suffix) \
  39     sscanf((s), prefix "%hhu.%hhu" suffix, &((version).v[0]), &((version).v[1]))
  40 
  41 #define schema_strdup_printf(prefix, version, suffix) \
  42     crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
  43 
  44 typedef struct {
  45     xmlRelaxNGPtr rng;
  46     xmlRelaxNGValidCtxtPtr valid;
  47     xmlRelaxNGParserCtxtPtr parser;
  48 } relaxng_ctx_cache_t;
  49 
  50 enum schema_validator_e {
  51     schema_validator_none,
  52     schema_validator_rng
  53 };
  54 
  55 struct schema_s {
  56     char *name;
  57     char *transform;
  58     void *cache;
  59     enum schema_validator_e validator;
  60     int after_transform;
  61     schema_version_t version;
  62     char *transform_enter;
  63     bool transform_onleave;
  64 };
  65 
  66 static struct schema_s *known_schemas = NULL;
  67 static int xml_schema_max = 0;
  68 static bool silent_logging = FALSE;
  69 
  70 static void
  71 xml_log(int priority, const char *fmt, ...)
     /* [previous][next][first][last][top][bottom][index][help] */
  72 G_GNUC_PRINTF(2, 3);
  73 
  74 static void
  75 xml_log(int priority, const char *fmt, ...)
  76 {
  77     va_list ap;
  78 
  79     va_start(ap, fmt);
  80     if (silent_logging == FALSE) {
  81         /* XXX should not this enable dechunking as well? */
  82         PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap);
  83     }
  84     va_end(ap);
  85 }
  86 
  87 static int
  88 xml_latest_schema_index(void)
     /* [previous][next][first][last][top][bottom][index][help] */
  89 {
  90     return xml_schema_max - 3; // index from 0, ignore "pacemaker-next"/"none"
  91 }
  92 
  93 static int
  94 xml_minimum_schema_index(void)
     /* [previous][next][first][last][top][bottom][index][help] */
  95 {
  96     static int best = 0;
  97     if (best == 0) {
  98         int lpc = 0;
  99 
 100         best = xml_latest_schema_index();
 101         for (lpc = best; lpc > 0; lpc--) {
 102             if (known_schemas[lpc].version.v[0]
 103                 < known_schemas[best].version.v[0]) {
 104                 return best;
 105             } else {
 106                 best = lpc;
 107             }
 108         }
 109         best = xml_latest_schema_index();
 110     }
 111     return best;
 112 }
 113 
 114 const char *
 115 xml_latest_schema(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 116 {
 117     return get_schema_name(xml_latest_schema_index());
 118 }
 119 
 120 static inline bool
 121 version_from_filename(const char *filename, schema_version_t *version)
     /* [previous][next][first][last][top][bottom][index][help] */
 122 {
 123     int rc = schema_scanf(filename, "pacemaker-", *version, ".rng");
 124 
 125     return (rc == 2);
 126 }
 127 
 128 static int
 129 schema_filter(const struct dirent *a)
     /* [previous][next][first][last][top][bottom][index][help] */
 130 {
 131     int rc = 0;
 132     schema_version_t version = SCHEMA_ZERO;
 133 
 134     if (strstr(a->d_name, "pacemaker-") != a->d_name) {
 135         /* crm_trace("%s - wrong prefix", a->d_name); */
 136 
 137     } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) {
 138         /* crm_trace("%s - wrong suffix", a->d_name); */
 139 
 140     } else if (!version_from_filename(a->d_name, &version)) {
 141         /* crm_trace("%s - wrong format", a->d_name); */
 142 
 143     } else {
 144         /* crm_debug("%s - candidate", a->d_name); */
 145         rc = 1;
 146     }
 147 
 148     return rc;
 149 }
 150 
 151 static int
 152 schema_sort(const struct dirent **a, const struct dirent **b)
     /* [previous][next][first][last][top][bottom][index][help] */
 153 {
 154     schema_version_t a_version = SCHEMA_ZERO;
 155     schema_version_t b_version = SCHEMA_ZERO;
 156 
 157     if (!version_from_filename(a[0]->d_name, &a_version)
 158         || !version_from_filename(b[0]->d_name, &b_version)) {
 159         // Shouldn't be possible, but makes static analysis happy
 160         return 0;
 161     }
 162 
 163     for (int i = 0; i < 2; ++i) {
 164         if (a_version.v[i] < b_version.v[i]) {
 165             return -1;
 166         } else if (a_version.v[i] > b_version.v[i]) {
 167             return 1;
 168         }
 169     }
 170     return 0;
 171 }
 172 
 173 /*!
 174  * \internal
 175  * \brief Add given schema + auxiliary data to internal bookkeeping.
 176  *
 177  * \note When providing \p version, should not be called directly but
 178  *       through \c add_schema_by_version.
 179  */
 180 static void
 181 add_schema(enum schema_validator_e validator, const schema_version_t *version,
     /* [previous][next][first][last][top][bottom][index][help] */
 182            const char *name, const char *transform,
 183            const char *transform_enter, bool transform_onleave,
 184            int after_transform)
 185 {
 186     int last = xml_schema_max;
 187     bool have_version = FALSE;
 188 
 189     xml_schema_max++;
 190     known_schemas = pcmk__realloc(known_schemas,
 191                                   xml_schema_max * sizeof(struct schema_s));
 192     CRM_ASSERT(known_schemas != NULL);
 193     memset(known_schemas+last, 0, sizeof(struct schema_s));
 194     known_schemas[last].validator = validator;
 195     known_schemas[last].after_transform = after_transform;
 196 
 197     for (int i = 0; i < 2; ++i) {
 198         known_schemas[last].version.v[i] = version->v[i];
 199         if (version->v[i]) {
 200             have_version = TRUE;
 201         }
 202     }
 203     if (have_version) {
 204         known_schemas[last].name = schema_strdup_printf("pacemaker-", *version, "");
 205     } else {
 206         CRM_ASSERT(name);
 207         schema_scanf(name, "%*[^-]-", known_schemas[last].version, "");
 208         known_schemas[last].name = strdup(name);
 209     }
 210 
 211     if (transform) {
 212         known_schemas[last].transform = strdup(transform);
 213     }
 214     if (transform_enter) {
 215         known_schemas[last].transform_enter = strdup(transform_enter);
 216     }
 217     known_schemas[last].transform_onleave = transform_onleave;
 218     if (after_transform == 0) {
 219         after_transform = xml_schema_max;  /* upgrade is a one-way */
 220     }
 221     known_schemas[last].after_transform = after_transform;
 222 
 223     if (known_schemas[last].after_transform < 0) {
 224         crm_debug("Added supported schema %d: %s",
 225                   last, known_schemas[last].name);
 226 
 227     } else if (known_schemas[last].transform) {
 228         crm_debug("Added supported schema %d: %s (upgrades to %d with %s.xsl)",
 229                   last, known_schemas[last].name,
 230                   known_schemas[last].after_transform,
 231                   known_schemas[last].transform);
 232 
 233     } else {
 234         crm_debug("Added supported schema %d: %s (upgrades to %d)",
 235                   last, known_schemas[last].name,
 236                   known_schemas[last].after_transform);
 237     }
 238 }
 239 
 240 /*!
 241  * \internal
 242  * \brief Add version-specified schema + auxiliary data to internal bookkeeping.
 243  * \return Standard Pacemaker return value (the only possible values are
 244  * \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise.
 245  *
 246  * \note There's no reliance on the particular order of schemas entering here.
 247  *
 248  * \par A bit of theory
 249  * We track 3 XSLT stylesheets that differ per usage:
 250  * - "upgrade":
 251  *   . sparsely spread over the sequence of all available schemas,
 252  *     as they are only relevant when major version of the schema
 253  *     is getting bumped -- in that case, it MUST be set
 254  *   . name convention:  upgrade-X.Y.xsl
 255  * - "upgrade-enter":
 256  *   . may only accompany "upgrade" occurrence, but doesn't need to
 257  *     be present anytime such one is, i.e., it MAY not be set when
 258  *     "upgrade" is
 259  *   . name convention:  upgrade-X.Y-enter.xsl,
 260  *     when not present: upgrade-enter.xsl
 261  * - "upgrade-leave":
 262  *   . like "upgrade-enter", but SHOULD be present whenever
 263  *     "upgrade-enter" is (and vice versa, but that's only
 264  *     to prevent confusion based on observing the files,
 265  *     it would get ignored regardless)
 266  *   . name convention:  (see "upgrade-enter")
 267  */
 268 static int
 269 add_schema_by_version(const schema_version_t *version, int next,
     /* [previous][next][first][last][top][bottom][index][help] */
 270                       bool transform_expected)
 271 {
 272     bool transform_onleave = FALSE;
 273     int rc = pcmk_rc_ok;
 274     struct stat s;
 275     char *xslt = NULL,
 276          *transform_upgrade = NULL,
 277          *transform_enter = NULL;
 278 
 279     /* prologue for further transform_expected handling */
 280     if (transform_expected) {
 281         /* check if there's suitable "upgrade" stylesheet */
 282         transform_upgrade = schema_strdup_printf("upgrade-", *version, );
 283         xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
 284                                        transform_upgrade);
 285     }
 286 
 287     if (!transform_expected) {
 288         /* jump directly to the end */
 289 
 290     } else if (stat(xslt, &s) == 0) {
 291         /* perhaps there's also a targeted "upgrade-enter" stylesheet */
 292         transform_enter = schema_strdup_printf("upgrade-", *version, "-enter");
 293         free(xslt);
 294         xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
 295                                        transform_enter);
 296         if (stat(xslt, &s) != 0) {
 297             /* or initially, at least a generic one */
 298             crm_debug("Upgrade-enter transform %s.xsl not found", xslt);
 299             free(xslt);
 300             free(transform_enter);
 301             transform_enter = strdup("upgrade-enter");
 302             xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
 303                                            transform_enter);
 304             if (stat(xslt, &s) != 0) {
 305                 crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt);
 306                 free(xslt);
 307                 xslt = NULL;
 308             }
 309         }
 310         /* xslt contains full path to "upgrade-enter" stylesheet */
 311         if (xslt != NULL) {
 312             /* then there should be "upgrade-leave" counterpart (enter->leave) */
 313             memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1);
 314             transform_onleave = (stat(xslt, &s) == 0);
 315             free(xslt);
 316         } else {
 317             free(transform_enter);
 318             transform_enter = NULL;
 319         }
 320 
 321     } else {
 322         crm_err("Upgrade transform %s not found", xslt);
 323         free(xslt);
 324         free(transform_upgrade);
 325         transform_upgrade = NULL;
 326         next = -1;
 327         rc = ENOENT;
 328     }
 329 
 330     add_schema(schema_validator_rng, version, NULL,
 331                transform_upgrade, transform_enter, transform_onleave, next);
 332 
 333     free(transform_upgrade);
 334     free(transform_enter);
 335 
 336     return rc;
 337 }
 338 
 339 static void
 340 wrap_libxslt(bool finalize)
     /* [previous][next][first][last][top][bottom][index][help] */
 341 {
 342     static xsltSecurityPrefsPtr secprefs;
 343     int ret = 0;
 344 
 345     /* security framework preferences */
 346     if (!finalize) {
 347         CRM_ASSERT(secprefs == NULL);
 348         secprefs = xsltNewSecurityPrefs();
 349         ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE,
 350                                    xsltSecurityForbid)
 351               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY,
 352                                      xsltSecurityForbid)
 353               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK,
 354                                      xsltSecurityForbid)
 355               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK,
 356                                      xsltSecurityForbid);
 357         if (ret != 0) {
 358             return;
 359         }
 360     } else {
 361         xsltFreeSecurityPrefs(secprefs);
 362         secprefs = NULL;
 363     }
 364 
 365     /* cleanup only */
 366     if (finalize) {
 367         xsltCleanupGlobals();
 368     }
 369 }
 370 
 371 /*!
 372  * \internal
 373  * \brief Load pacemaker schemas into cache
 374  *
 375  * \note This currently also serves as an entry point for the
 376  *       generic initialization of the libxslt library.
 377  */
 378 void
 379 crm_schema_init(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 380 {
 381     int lpc, max;
 382     char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng);
 383     struct dirent **namelist = NULL;
 384     const schema_version_t zero = SCHEMA_ZERO;
 385 
 386     wrap_libxslt(false);
 387 
 388     max = scandir(base, &namelist, schema_filter, schema_sort);
 389     if (max < 0) {
 390         crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno);
 391         free(base);
 392 
 393     } else {
 394         free(base);
 395         for (lpc = 0; lpc < max; lpc++) {
 396             bool transform_expected = FALSE;
 397             int next = 0;
 398             schema_version_t version = SCHEMA_ZERO;
 399 
 400             if (!version_from_filename(namelist[lpc]->d_name, &version)) {
 401                 // Shouldn't be possible, but makes static analysis happy
 402                 crm_err("Skipping schema '%s': could not parse version",
 403                         namelist[lpc]->d_name);
 404                 continue;
 405             }
 406             if ((lpc + 1) < max) {
 407                 schema_version_t next_version = SCHEMA_ZERO;
 408 
 409                 if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
 410                         && (version.v[0] < next_version.v[0])) {
 411                     transform_expected = TRUE;
 412                 }
 413 
 414             } else {
 415                 next = -1;
 416             }
 417             if (add_schema_by_version(&version, next, transform_expected)
 418                     == ENOENT) {
 419                 break;
 420             }
 421         }
 422 
 423         for (lpc = 0; lpc < max; lpc++) {
 424             free(namelist[lpc]);
 425         }
 426         free(namelist);
 427     }
 428 
 429     add_schema(schema_validator_rng, &zero, "pacemaker-next",
 430                NULL, NULL, FALSE, -1);
 431 
 432     add_schema(schema_validator_none, &zero, "none", NULL, NULL, FALSE, -1);
 433 }
 434 
 435 #if 0
 436 static void
 437 relaxng_invalid_stderr(void *userData, xmlErrorPtr error)
     /* [previous][next][first][last][top][bottom][index][help] */
 438 {
 439     /*
 440        Structure xmlError
 441        struct _xmlError {
 442        int      domain  : What part of the library raised this er
 443        int      code    : The error code, e.g. an xmlParserError
 444        char *   message : human-readable informative error messag
 445        xmlErrorLevel    level   : how consequent is the error
 446        char *   file    : the filename
 447        int      line    : the line number if available
 448        char *   str1    : extra string information
 449        char *   str2    : extra string information
 450        char *   str3    : extra string information
 451        int      int1    : extra number information
 452        int      int2    : column number of the error or 0 if N/A
 453        void *   ctxt    : the parser context if available
 454        void *   node    : the node in the tree
 455        }
 456      */
 457     crm_err("Structured error: line=%d, level=%d %s", error->line, error->level, error->message);
 458 }
 459 #endif
 460 
 461 static gboolean
 462 validate_with_relaxng(xmlDocPtr doc, gboolean to_logs, const char *relaxng_file,
     /* [previous][next][first][last][top][bottom][index][help] */
 463                       relaxng_ctx_cache_t **cached_ctx)
 464 {
 465     int rc = 0;
 466     gboolean valid = TRUE;
 467     relaxng_ctx_cache_t *ctx = NULL;
 468 
 469     CRM_CHECK(doc != NULL, return FALSE);
 470     CRM_CHECK(relaxng_file != NULL, return FALSE);
 471 
 472     if (cached_ctx && *cached_ctx) {
 473         ctx = *cached_ctx;
 474 
 475     } else {
 476         crm_debug("Creating RNG parser context");
 477         ctx = calloc(1, sizeof(relaxng_ctx_cache_t));
 478 
 479         xmlLoadExtDtdDefaultValue = 1;
 480         ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
 481         CRM_CHECK(ctx->parser != NULL, goto cleanup);
 482 
 483         if (to_logs) {
 484             xmlRelaxNGSetParserErrors(ctx->parser,
 485                                       (xmlRelaxNGValidityErrorFunc) xml_log,
 486                                       (xmlRelaxNGValidityWarningFunc) xml_log,
 487                                       GUINT_TO_POINTER(LOG_ERR));
 488         } else {
 489             xmlRelaxNGSetParserErrors(ctx->parser,
 490                                       (xmlRelaxNGValidityErrorFunc) fprintf,
 491                                       (xmlRelaxNGValidityWarningFunc) fprintf,
 492                                       stderr);
 493         }
 494 
 495         ctx->rng = xmlRelaxNGParse(ctx->parser);
 496         CRM_CHECK(ctx->rng != NULL,
 497                   crm_err("Could not find/parse %s", relaxng_file);
 498                   goto cleanup);
 499 
 500         ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng);
 501         CRM_CHECK(ctx->valid != NULL, goto cleanup);
 502 
 503         if (to_logs) {
 504             xmlRelaxNGSetValidErrors(ctx->valid,
 505                                      (xmlRelaxNGValidityErrorFunc) xml_log,
 506                                      (xmlRelaxNGValidityWarningFunc) xml_log,
 507                                      GUINT_TO_POINTER(LOG_ERR));
 508         } else {
 509             xmlRelaxNGSetValidErrors(ctx->valid,
 510                                      (xmlRelaxNGValidityErrorFunc) fprintf,
 511                                      (xmlRelaxNGValidityWarningFunc) fprintf,
 512                                      stderr);
 513         }
 514     }
 515 
 516     /* xmlRelaxNGSetValidStructuredErrors( */
 517     /*  valid, relaxng_invalid_stderr, valid); */
 518 
 519     xmlLineNumbersDefault(1);
 520     rc = xmlRelaxNGValidateDoc(ctx->valid, doc);
 521     if (rc > 0) {
 522         valid = FALSE;
 523 
 524     } else if (rc < 0) {
 525         crm_err("Internal libxml error during validation");
 526     }
 527 
 528   cleanup:
 529 
 530     if (cached_ctx) {
 531         *cached_ctx = ctx;
 532 
 533     } else {
 534         if (ctx->parser != NULL) {
 535             xmlRelaxNGFreeParserCtxt(ctx->parser);
 536         }
 537         if (ctx->valid != NULL) {
 538             xmlRelaxNGFreeValidCtxt(ctx->valid);
 539         }
 540         if (ctx->rng != NULL) {
 541             xmlRelaxNGFree(ctx->rng);
 542         }
 543         free(ctx);
 544     }
 545 
 546     return valid;
 547 }
 548 
 549 /*!
 550  * \internal
 551  * \brief Clean up global memory associated with XML schemas
 552  */
 553 void
 554 crm_schema_cleanup(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 555 {
 556     int lpc;
 557     relaxng_ctx_cache_t *ctx = NULL;
 558 
 559     for (lpc = 0; lpc < xml_schema_max; lpc++) {
 560 
 561         switch (known_schemas[lpc].validator) {
 562             case schema_validator_none: // not cached
 563                 break;
 564             case schema_validator_rng: // cached
 565                 ctx = (relaxng_ctx_cache_t *) known_schemas[lpc].cache;
 566                 if (ctx == NULL) {
 567                     break;
 568                 }
 569                 if (ctx->parser != NULL) {
 570                     xmlRelaxNGFreeParserCtxt(ctx->parser);
 571                 }
 572                 if (ctx->valid != NULL) {
 573                     xmlRelaxNGFreeValidCtxt(ctx->valid);
 574                 }
 575                 if (ctx->rng != NULL) {
 576                     xmlRelaxNGFree(ctx->rng);
 577                 }
 578                 free(ctx);
 579                 known_schemas[lpc].cache = NULL;
 580                 break;
 581         }
 582         free(known_schemas[lpc].name);
 583         free(known_schemas[lpc].transform);
 584         free(known_schemas[lpc].transform_enter);
 585     }
 586     free(known_schemas);
 587     known_schemas = NULL;
 588 
 589     wrap_libxslt(true);
 590 }
 591 
 592 static gboolean
 593 validate_with(xmlNode *xml, int method, gboolean to_logs)
     /* [previous][next][first][last][top][bottom][index][help] */
 594 {
 595     xmlDocPtr doc = NULL;
 596     gboolean valid = FALSE;
 597     char *file = NULL;
 598 
 599     if (method < 0) {
 600         return FALSE;
 601     }
 602 
 603     if (known_schemas[method].validator == schema_validator_none) {
 604         return TRUE;
 605     }
 606 
 607     CRM_CHECK(xml != NULL, return FALSE);
 608     doc = getDocPtr(xml);
 609     file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
 610                                    known_schemas[method].name);
 611 
 612     crm_trace("Validating with: %s (type=%d)",
 613               crm_str(file), known_schemas[method].validator);
 614     switch (known_schemas[method].validator) {
 615         case schema_validator_rng:
 616             valid =
 617                 validate_with_relaxng(doc, to_logs, file,
 618                                       (relaxng_ctx_cache_t **) & (known_schemas[method].cache));
 619             break;
 620         default:
 621             crm_err("Unknown validator type: %d",
 622                     known_schemas[method].validator);
 623             break;
 624     }
 625 
 626     free(file);
 627     return valid;
 628 }
 629 
 630 static bool
 631 validate_with_silent(xmlNode *xml, int method)
     /* [previous][next][first][last][top][bottom][index][help] */
 632 {
 633     bool rc, sl_backup = silent_logging;
 634     silent_logging = TRUE;
 635     rc = validate_with(xml, method, TRUE);
 636     silent_logging = sl_backup;
 637     return rc;
 638 }
 639 
 640 static void
 641 dump_file(const char *filename)
     /* [previous][next][first][last][top][bottom][index][help] */
 642 {
 643 
 644     FILE *fp = NULL;
 645     int ch, line = 0;
 646 
 647     CRM_CHECK(filename != NULL, return);
 648 
 649     fp = fopen(filename, "r");
 650     if (fp == NULL) {
 651         crm_perror(LOG_ERR, "Could not open %s for reading", filename);
 652         return;
 653     }
 654 
 655     fprintf(stderr, "%4d ", ++line);
 656     do {
 657         ch = getc(fp);
 658         if (ch == EOF) {
 659             putc('\n', stderr);
 660             break;
 661         } else if (ch == '\n') {
 662             fprintf(stderr, "\n%4d ", ++line);
 663         } else {
 664             putc(ch, stderr);
 665         }
 666     } while (1);
 667 
 668     fclose(fp);
 669 }
 670 
 671 gboolean
 672 validate_xml_verbose(xmlNode *xml_blob)
     /* [previous][next][first][last][top][bottom][index][help] */
 673 {
 674     int fd = 0;
 675     xmlDoc *doc = NULL;
 676     xmlNode *xml = NULL;
 677     gboolean rc = FALSE;
 678     char *filename = NULL;
 679 
 680     filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
 681 
 682     umask(S_IWGRP | S_IWOTH | S_IROTH);
 683     fd = mkstemp(filename);
 684     write_xml_fd(xml_blob, filename, fd, FALSE);
 685 
 686     dump_file(filename);
 687 
 688     doc = xmlParseFile(filename);
 689     xml = xmlDocGetRootElement(doc);
 690     rc = validate_xml(xml, NULL, FALSE);
 691     free_xml(xml);
 692 
 693     unlink(filename);
 694     free(filename);
 695 
 696     return rc;
 697 }
 698 
 699 gboolean
 700 validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
     /* [previous][next][first][last][top][bottom][index][help] */
 701 {
 702     int version = 0;
 703 
 704     if (validation == NULL) {
 705         validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION);
 706     }
 707 
 708     if (validation == NULL) {
 709         int lpc = 0;
 710         bool valid = FALSE;
 711 
 712         for (lpc = 0; lpc < xml_schema_max; lpc++) {
 713             if (validate_with(xml_blob, lpc, FALSE)) {
 714                 valid = TRUE;
 715                 crm_xml_add(xml_blob, XML_ATTR_VALIDATION,
 716                             known_schemas[lpc].name);
 717                 crm_info("XML validated against %s", known_schemas[lpc].name);
 718                 if(known_schemas[lpc].after_transform == 0) {
 719                     break;
 720                 }
 721             }
 722         }
 723 
 724         return valid;
 725     }
 726 
 727     version = get_schema_version(validation);
 728     if (strcmp(validation, "none") == 0) {
 729         return TRUE;
 730     } else if (version < xml_schema_max) {
 731         return validate_with(xml_blob, version, to_logs);
 732     }
 733 
 734     crm_err("Unknown validator: %s", validation);
 735     return FALSE;
 736 }
 737 
 738 #if HAVE_LIBXSLT
 739 
 740 static void
 741 cib_upgrade_err(void *ctx, const char *fmt, ...)
     /* [previous][next][first][last][top][bottom][index][help] */
 742 G_GNUC_PRINTF(2, 3);
 743 
 744 /* With this arrangement, an attempt to identify the message severity
 745    as explicitly signalled directly from XSLT is performed in rather
 746    a smart way (no reliance on formatting string + arguments being
 747    always specified as ["%s", purposeful_string], as it can also be
 748    ["%s: %s", some_prefix, purposeful_string] etc. so every argument
 749    pertaining %s specifier is investigated), and if such a mark found,
 750    the respective level is determined and, when the messages are to go
 751    to the native logs, the mark itself gets dropped
 752    (by the means of string shift).
 753 
 754    NOTE: whether the native logging is the right sink is decided per
 755          the ctx parameter -- NULL denotes this case, otherwise it
 756          carries a pointer to the numeric expression of the desired
 757          target logging level (messages with higher level will be
 758          suppressed)
 759 
 760    NOTE: on some architectures, this string shift may not have any
 761          effect, but that's an acceptable tradeoff
 762 
 763    The logging level for not explicitly designated messages
 764    (suspicious, likely internal errors or some runaways) is
 765    LOG_WARNING.
 766  */
 767 static void
 768 cib_upgrade_err(void *ctx, const char *fmt, ...)
 769 {
 770     va_list ap, aq;
 771     char *arg_cur;
 772 
 773     bool found = FALSE;
 774     const char *fmt_iter = fmt;
 775     uint8_t msg_log_level = LOG_WARNING;  /* default for runaway messages */
 776     const unsigned * log_level = (const unsigned *) ctx;
 777     enum {
 778         escan_seennothing,
 779         escan_seenpercent,
 780     } scan_state = escan_seennothing;
 781 
 782     va_start(ap, fmt);
 783     va_copy(aq, ap);
 784 
 785     while (!found && *fmt_iter != '\0') {
 786         /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */
 787         switch (*fmt_iter++) {
 788         case '%':
 789             if (scan_state == escan_seennothing) {
 790                 scan_state = escan_seenpercent;
 791             } else if (scan_state == escan_seenpercent) {
 792                 scan_state = escan_seennothing;
 793             }
 794             break;
 795         case 's':
 796             if (scan_state == escan_seenpercent) {
 797                 scan_state = escan_seennothing;
 798                 arg_cur = va_arg(aq, char *);
 799                 if (arg_cur != NULL) {
 800                     switch (arg_cur[0]) {
 801                     case 'W':
 802                         if (!strncmp(arg_cur, "WARNING: ",
 803                                      sizeof("WARNING: ") - 1)) {
 804                             msg_log_level = LOG_WARNING;
 805                         }
 806                         if (ctx == NULL) {
 807                             memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1,
 808                                     strlen(arg_cur + sizeof("WARNING: ") - 1) + 1);
 809                         }
 810                         found = TRUE;
 811                         break;
 812                     case 'I':
 813                         if (!strncmp(arg_cur, "INFO: ",
 814                                      sizeof("INFO: ") - 1)) {
 815                             msg_log_level = LOG_INFO;
 816                         }
 817                         if (ctx == NULL) {
 818                             memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1,
 819                                     strlen(arg_cur + sizeof("INFO: ") - 1) + 1);
 820                         }
 821                         found = TRUE;
 822                         break;
 823                     case 'D':
 824                         if (!strncmp(arg_cur, "DEBUG: ",
 825                                      sizeof("DEBUG: ") - 1)) {
 826                             msg_log_level = LOG_DEBUG;
 827                         }
 828                         if (ctx == NULL) {
 829                             memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1,
 830                                     strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1);
 831                         }
 832                         found = TRUE;
 833                         break;
 834                     }
 835                 }
 836             }
 837             break;
 838         case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.':
 839         case '0': case '1': case '2': case '3': case '4':
 840         case '5': case '6': case '7': case '8': case '9':
 841         case '*':
 842             break;
 843         case 'l':
 844         case 'z':
 845         case 't':
 846         case 'j':
 847         case 'd': case 'i':
 848         case 'o':
 849         case 'u':
 850         case 'x': case 'X':
 851         case 'e': case 'E':
 852         case 'f': case 'F':
 853         case 'g': case 'G':
 854         case 'a': case 'A':
 855         case 'c':
 856         case 'p':
 857             if (scan_state == escan_seenpercent) {
 858                 (void) va_arg(aq, void *);  /* skip forward */
 859                 scan_state = escan_seennothing;
 860             }
 861             break;
 862         default:
 863             scan_state = escan_seennothing;
 864             break;
 865         }
 866     }
 867 
 868     if (log_level != NULL) {
 869         /* intention of the following offset is:
 870            cibadmin -V -> start showing INFO labelled messages */
 871         if (*log_level + 4 >= msg_log_level) {
 872             vfprintf(stderr, fmt, ap);
 873         }
 874     } else {
 875         PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap);
 876     }
 877 
 878     va_end(aq);
 879     va_end(ap);
 880 }
 881 
 882 
 883 /* Denotes temporary emergency fix for "xmldiff'ing not text-node-ready";
 884    proper fix is most likely to teach __xml_diff_object and friends to
 885    deal with XML_TEXT_NODE (and more?), i.e., those nodes currently
 886    missing "_private" field (implicitly as NULL) which clashes with
 887    unchecked accesses (e.g. in __xml_offset) -- the outcome may be that
 888    those unexpected XML nodes will simply be ignored for the purpose of
 889    diff'ing, or it may be made more robust, or per the user's preference
 890    (which then may be exposed as crm_diff switch).
 891 
 892    Said XML_TEXT_NODE may appear unexpectedly due to how upgrade-2.10.xsl
 893    is arranged.
 894 
 895    The emergency fix is simple: reparse XSLT output with blank-ignoring
 896    parser. */
 897 #ifndef PCMK_SCHEMAS_EMERGENCY_XSLT
 898 #define PCMK_SCHEMAS_EMERGENCY_XSLT 1
 899 #endif
 900 
 901 static xmlNode *
 902 apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs)
     /* [previous][next][first][last][top][bottom][index][help] */
 903 {
 904     char *xform = NULL;
 905     xmlNode *out = NULL;
 906     xmlDocPtr res = NULL;
 907     xmlDocPtr doc = NULL;
 908     xsltStylesheet *xslt = NULL;
 909 #if PCMK_SCHEMAS_EMERGENCY_XSLT != 0
 910     xmlChar *emergency_result;
 911     int emergency_txt_len;
 912     int emergency_res;
 913 #endif
 914 
 915     CRM_CHECK(xml != NULL, return FALSE);
 916     doc = getDocPtr(xml);
 917     xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
 918                                     transform);
 919 
 920     xmlLoadExtDtdDefaultValue = 1;
 921     xmlSubstituteEntitiesDefault(1);
 922 
 923     /* for capturing, e.g., what's emitted via <xsl:message> */
 924     if (to_logs) {
 925         xsltSetGenericErrorFunc(NULL, cib_upgrade_err);
 926     } else {
 927         xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err);
 928     }
 929 
 930     xslt = xsltParseStylesheetFile((pcmkXmlStr) xform);
 931     CRM_CHECK(xslt != NULL, goto cleanup);
 932 
 933     res = xsltApplyStylesheet(xslt, doc, NULL);
 934     CRM_CHECK(res != NULL, goto cleanup);
 935 
 936     xsltSetGenericErrorFunc(NULL, NULL);  /* restore default one */
 937 
 938 
 939 #if PCMK_SCHEMAS_EMERGENCY_XSLT != 0
 940     emergency_res = xsltSaveResultToString(&emergency_result,
 941                                            &emergency_txt_len, res, xslt);
 942     xmlFreeDoc(res);
 943     CRM_CHECK(emergency_res == 0, goto cleanup);
 944     out = string2xml((const char *) emergency_result);
 945     free(emergency_result);
 946 #else
 947     out = xmlDocGetRootElement(res);
 948 #endif
 949 
 950   cleanup:
 951     if (xslt) {
 952         xsltFreeStylesheet(xslt);
 953     }
 954 
 955     free(xform);
 956 
 957     return out;
 958 }
 959 
 960 /*!
 961  * \internal
 962  * \brief Possibly full enter->upgrade->leave trip per internal bookkeeping.
 963  *
 964  * \note Only emits warnings about enter/leave phases in case of issues.
 965  */
 966 static xmlNode *
 967 apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs)
     /* [previous][next][first][last][top][bottom][index][help] */
 968 {
 969     bool transform_onleave = schema->transform_onleave;
 970     char *transform_leave;
 971     xmlNode *upgrade = NULL,
 972             *final = NULL;
 973 
 974     if (schema->transform_enter) {
 975         crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl",
 976                   schema->name, schema->transform_enter);
 977         upgrade = apply_transformation(xml, schema->transform_enter, to_logs);
 978         if (upgrade == NULL) {
 979             crm_warn("Upgrade-enter transformation %s.xsl failed",
 980                      schema->transform_enter);
 981             transform_onleave = FALSE;
 982         }
 983     }
 984     if (upgrade == NULL) {
 985         upgrade = xml;
 986     }
 987 
 988     crm_debug("Upgrading %s-style configuration, main phase with %s.xsl",
 989               schema->name, schema->transform);
 990     final = apply_transformation(upgrade, schema->transform, to_logs);
 991     if (upgrade != xml) {
 992         free_xml(upgrade);
 993         upgrade = NULL;
 994     }
 995 
 996     if (final != NULL && transform_onleave) {
 997         upgrade = final;
 998         /* following condition ensured in add_schema_by_version */
 999         CRM_ASSERT(schema->transform_enter != NULL);
1000         transform_leave = strdup(schema->transform_enter);
1001         /* enter -> leave */
1002         memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1);
1003         crm_debug("Upgrading %s-style configuration, post-upgrade phase with %s.xsl",
1004                   schema->name, transform_leave);
1005         final = apply_transformation(upgrade, transform_leave, to_logs);
1006         if (final == NULL) {
1007             crm_warn("Upgrade-leave transformation %s.xsl failed", transform_leave);
1008             final = upgrade;
1009         } else {
1010             free_xml(upgrade);
1011         }
1012         free(transform_leave);
1013     }
1014 
1015     return final;
1016 }
1017 
1018 #endif  /* HAVE_LIBXSLT */
1019 
1020 const char *
1021 get_schema_name(int version)
     /* [previous][next][first][last][top][bottom][index][help] */
1022 {
1023     if (version < 0 || version >= xml_schema_max) {
1024         return "unknown";
1025     }
1026     return known_schemas[version].name;
1027 }
1028 
1029 int
1030 get_schema_version(const char *name)
     /* [previous][next][first][last][top][bottom][index][help] */
1031 {
1032     int lpc = 0;
1033 
1034     if (name == NULL) {
1035         name = "none";
1036     }
1037     for (; lpc < xml_schema_max; lpc++) {
1038         if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) {
1039             return lpc;
1040         }
1041     }
1042     return -1;
1043 }
1044 
1045 /* set which validation to use */
1046 int
1047 update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform,
     /* [previous][next][first][last][top][bottom][index][help] */
1048                   gboolean to_logs)
1049 {
1050     xmlNode *xml = NULL;
1051     char *value = NULL;
1052     int max_stable_schemas = xml_latest_schema_index();
1053     int lpc = 0, match = -1, rc = pcmk_ok;
1054     int next = -1;  /* -1 denotes "inactive" value */
1055 
1056     CRM_CHECK(best != NULL, return -EINVAL);
1057     *best = 0;
1058 
1059     CRM_CHECK(xml_blob != NULL, return -EINVAL);
1060     CRM_CHECK(*xml_blob != NULL, return -EINVAL);
1061 
1062     xml = *xml_blob;
1063     value = crm_element_value_copy(xml, XML_ATTR_VALIDATION);
1064 
1065     if (value != NULL) {
1066         match = get_schema_version(value);
1067 
1068         lpc = match;
1069         if (lpc >= 0 && transform == FALSE) {
1070             *best = lpc++;
1071 
1072         } else if (lpc < 0) {
1073             crm_debug("Unknown validation schema");
1074             lpc = 0;
1075         }
1076     }
1077 
1078     if (match >= max_stable_schemas) {
1079         /* nothing to do */
1080         free(value);
1081         *best = match;
1082         return pcmk_ok;
1083     }
1084 
1085     while (lpc <= max_stable_schemas) {
1086         crm_debug("Testing '%s' validation (%d of %d)",
1087                   known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>",
1088                   lpc, max_stable_schemas);
1089 
1090         if (validate_with(xml, lpc, to_logs) == FALSE) {
1091             if (next != -1) {
1092                 crm_info("Configuration not valid for schema: %s",
1093                          known_schemas[lpc].name);
1094                 next = -1;
1095             } else {
1096                 crm_trace("%s validation failed",
1097                           known_schemas[lpc].name ? known_schemas[lpc].name : "<unset>");
1098             }
1099             if (*best) {
1100                 /* we've satisfied the validation, no need to check further */
1101                 break;
1102             }
1103             rc = -pcmk_err_schema_validation;
1104 
1105         } else {
1106             if (next != -1) {
1107                 crm_debug("Configuration valid for schema: %s",
1108                           known_schemas[next].name);
1109                 next = -1;
1110             }
1111             rc = pcmk_ok;
1112         }
1113 
1114         if (rc == pcmk_ok) {
1115             *best = lpc;
1116         }
1117 
1118         if (rc == pcmk_ok && transform) {
1119             xmlNode *upgrade = NULL;
1120             next = known_schemas[lpc].after_transform;
1121 
1122             if (next <= lpc) {
1123                 /* There is no next version, or next would regress */
1124                 crm_trace("Stopping at %s", known_schemas[lpc].name);
1125                 break;
1126 
1127             } else if (max > 0 && (lpc == max || next > max)) {
1128                 crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)",
1129                           known_schemas[lpc].name, lpc, next, max);
1130                 break;
1131 
1132             } else if (known_schemas[lpc].transform == NULL
1133                        /* possibly avoid transforming when readily valid
1134                           (in general more restricted when crossing the major
1135                           version boundary, as X.0 "transitional" version is
1136                           expected to be more strict than it's successors that
1137                           may re-allow constructs from previous major line) */
1138                        || validate_with_silent(xml, next)) {
1139                 crm_debug("%s-style configuration is also valid for %s",
1140                            known_schemas[lpc].name, known_schemas[next].name);
1141 
1142                 lpc = next;
1143 
1144             } else {
1145                 crm_debug("Upgrading %s-style configuration to %s with %s.xsl",
1146                            known_schemas[lpc].name, known_schemas[next].name,
1147                            known_schemas[lpc].transform);
1148 
1149 #if HAVE_LIBXSLT
1150                 upgrade = apply_upgrade(xml, &known_schemas[lpc], to_logs);
1151 #endif
1152                 if (upgrade == NULL) {
1153                     crm_err("Transformation %s.xsl failed",
1154                             known_schemas[lpc].transform);
1155                     rc = -pcmk_err_transform_failed;
1156 
1157                 } else if (validate_with(upgrade, next, to_logs)) {
1158                     crm_info("Transformation %s.xsl successful",
1159                              known_schemas[lpc].transform);
1160                     lpc = next;
1161                     *best = next;
1162                     free_xml(xml);
1163                     xml = upgrade;
1164                     rc = pcmk_ok;
1165 
1166                 } else {
1167                     crm_err("Transformation %s.xsl did not produce a valid configuration",
1168                             known_schemas[lpc].transform);
1169                     crm_log_xml_info(upgrade, "transform:bad");
1170                     free_xml(upgrade);
1171                     rc = -pcmk_err_schema_validation;
1172                 }
1173                 next = -1;
1174             }
1175         }
1176 
1177         if (transform == FALSE || rc != pcmk_ok) {
1178             /* we need some progress! */
1179             lpc++;
1180         }
1181     }
1182 
1183     if (*best > match && *best) {
1184         crm_info("%s the configuration from %s to %s",
1185                    transform?"Transformed":"Upgraded",
1186                    value ? value : "<none>", known_schemas[*best].name);
1187         crm_xml_add(xml, XML_ATTR_VALIDATION, known_schemas[*best].name);
1188     }
1189 
1190     *xml_blob = xml;
1191     free(value);
1192     return rc;
1193 }
1194 
1195 gboolean
1196 cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
     /* [previous][next][first][last][top][bottom][index][help] */
1197 {
1198     gboolean rc = TRUE;
1199     const char *value = crm_element_value(*xml, XML_ATTR_VALIDATION);
1200     char *const orig_value = strdup(value == NULL ? "(none)" : value);
1201 
1202     int version = get_schema_version(value);
1203     int orig_version = version;
1204     int min_version = xml_minimum_schema_index();
1205 
1206     if (version < min_version) {
1207         // Current configuration schema is not acceptable, try to update
1208         xmlNode *converted = NULL;
1209 
1210         converted = copy_xml(*xml);
1211         update_validation(&converted, &version, 0, TRUE, to_logs);
1212 
1213         value = crm_element_value(converted, XML_ATTR_VALIDATION);
1214         if (version < min_version) {
1215             // Updated configuration schema is still not acceptable
1216 
1217             if (version < orig_version || orig_version == -1) {
1218                 // We couldn't validate any schema at all
1219                 if (to_logs) {
1220                     pcmk__config_err("Cannot upgrade configuration (claiming "
1221                                      "schema %s) to at least %s because it "
1222                                      "does not validate with any schema from "
1223                                      "%s to %s",
1224                                      orig_value,
1225                                      get_schema_name(min_version),
1226                                      get_schema_name(orig_version),
1227                                      xml_latest_schema());
1228                 } else {
1229                     fprintf(stderr, "Cannot upgrade configuration (claiming "
1230                                     "schema %s) to at least %s because it "
1231                                     "does not validate with any schema from "
1232                                     "%s to %s\n",
1233                                     orig_value,
1234                                     get_schema_name(min_version),
1235                                     get_schema_name(orig_version),
1236                                     xml_latest_schema());
1237                 }
1238             } else {
1239                 // We updated configuration successfully, but still too low
1240                 if (to_logs) {
1241                     pcmk__config_err("Cannot upgrade configuration (claiming "
1242                                      "schema %s) to at least %s because it "
1243                                      "would not upgrade past %s",
1244                                      orig_value,
1245                                      get_schema_name(min_version),
1246                                      crm_str(value));
1247                 } else {
1248                     fprintf(stderr, "Cannot upgrade configuration (claiming "
1249                                     "schema %s) to at least %s because it "
1250                                     "would not upgrade past %s\n",
1251                                     orig_value,
1252                                     get_schema_name(min_version),
1253                                     crm_str(value));
1254                 }
1255             }
1256 
1257             free_xml(converted);
1258             converted = NULL;
1259             rc = FALSE;
1260 
1261         } else {
1262             // Updated configuration schema is acceptable
1263             free_xml(*xml);
1264             *xml = converted;
1265 
1266             if (version < xml_latest_schema_index()) {
1267                 if (to_logs) {
1268                     pcmk__config_warn("Configuration with schema %s was "
1269                                       "internally upgraded to acceptable (but "
1270                                       "not most recent) %s",
1271                                       orig_value, get_schema_name(version));
1272                 }
1273             } else {
1274                 if (to_logs) {
1275                     crm_info("Configuration with schema %s was internally "
1276                              "upgraded to latest version %s",
1277                              orig_value, get_schema_name(version));
1278                 }
1279             }
1280         }
1281 
1282     } else if (version >= get_schema_version("none")) {
1283         // Schema validation is disabled
1284         if (to_logs) {
1285             pcmk__config_warn("Schema validation of configuration is disabled "
1286                               "(enabling is encouraged and prevents common "
1287                               "misconfigurations)");
1288 
1289         } else {
1290             fprintf(stderr, "Schema validation of configuration is disabled "
1291                             "(enabling is encouraged and prevents common "
1292                             "misconfigurations)\n");
1293         }
1294     }
1295 
1296     if (best_version) {
1297         *best_version = version;
1298     }
1299 
1300     free(orig_value);
1301     return rc;
1302 }

/* [previous][next][first][last][top][bottom][index][help] */