FOSSology  4.5.1
Open Source License Compliance by Open Source Software
clixml.php
1 <?php
2 /*
3  SPDX-FileCopyrightText: © 2021 Siemens AG
4 
5  SPDX-License-Identifier: GPL-2.0-only
6 */
7 namespace Fossology\CliXml;
8 
22 use Twig\Environment;
23 
24 include_once(__DIR__ . "/version.php");
25 include_once(__DIR__ . "/services.php");
26 
27 class CliXml extends Agent
28 {
29 
30  const OUTPUT_FORMAT_KEY = "outputFormat";
31  const DEFAULT_OUTPUT_FORMAT = "clixml";
32  const AVAILABLE_OUTPUT_FORMATS = "xml";
33  const UPLOAD_ADDS = "uploadsAdd";
34 
38  private $additionalUploads = [];
39 
41  private $uploadDao;
42 
44  protected $dbManager;
45 
47  protected $licenseDao;
48 
50  protected $renderer;
51 
53  protected $uri;
54 
56  protected $packageName;
57 
62 
67 
76 
80  private $reportutils;
81 
83  protected $outputFormat = self::DEFAULT_OUTPUT_FORMAT;
84 
85  function __construct()
86  {
87  $args = getopt("", array(self::UPLOAD_ADDS.'::'));
88 
89  if (array_key_exists(self::UPLOAD_ADDS, $args)) {
90  $uploadsString = $args[self::UPLOAD_ADDS];
91  if (!empty($uploadsString)) {
92  $this->additionalUploads = explode(',', $uploadsString);
93  }
94  }
95 
96  parent::__construct('clixml', AGENT_VERSION, AGENT_REV);
97 
98  $this->uploadDao = $this->container->get('dao.upload');
99  $this->dbManager = $this->container->get('db.manager');
100  $this->licenseDao = $this->container->get('dao.license');
101  $this->renderer = $this->container->get('twig.environment');
102  $this->renderer->setCache(false);
103 
104  $this->cpClearedGetter = new XpClearedGetter("copyright", "statement");
105  $this->eccClearedGetter = new XpClearedGetter("ecc", "ecc");
106  $this->ipraClearedGetter = new XpClearedGetter("ipra", "ipra");
107  $this->licenseIrrelevantGetter = new LicenseIrrelevantGetter();
108  $this->licenseIrrelevantGetterComments = new LicenseIrrelevantGetter(false);
109  $this->licenseDNUGetter = new LicenseDNUGetter();
110  $this->licenseDNUCommentGetter = new LicenseDNUGetter(false);
111  $this->licenseClearedGetter = new LicenseClearedGetter();
112  $this->licenseMainGetter = new LicenseMainGetter();
113  $this->obligationsGetter = new ObligationsGetter();
114  $this->otherGetter = new OtherGetter();
115  $this->reportutils = new ReportUtils();
116  $this->agentSpecifLongOptions[] = self::UPLOAD_ADDS.':';
117  $this->agentSpecifLongOptions[] = self::OUTPUT_FORMAT_KEY.':';
118  }
119 
127  protected function preWorkOnArgsFlp($args,$key1,$key2)
128  {
129  $needle = ' --'.$key2.'=';
130  if (array_key_exists($key1,$args) && strpos($args[$key1],$needle) !== false) {
131  $exploded = explode($needle,$args[$key1]);
132  $args[$key1] = trim($exploded[0]);
133  $args[$key2] = trim($exploded[1]);
134  }
135  return $args;
136  }
137 
143  protected function preWorkOnArgs($args)
144  {
145  if ((!array_key_exists(self::OUTPUT_FORMAT_KEY,$args)
146  || $args[self::OUTPUT_FORMAT_KEY] === "")
147  && array_key_exists(self::UPLOAD_ADDS,$args)) {
148 
149  $args = $this->preWorkOnArgsFlp($args,self::UPLOAD_ADDS,self::OUTPUT_FORMAT_KEY);
150  } else {
151  if (!array_key_exists(self::UPLOAD_ADDS,$args) || $args[self::UPLOAD_ADDS] === "") {
152  $args = $this->preWorkOnArgsFlp($args,self::UPLOAD_ADDS,self::OUTPUT_FORMAT_KEY);
153  }
154  }
155  return $args;
156  }
157 
158  function processUploadId($uploadId)
159  {
161 
162  $args = $this->preWorkOnArgs($this->args);
163 
164  if (array_key_exists(self::OUTPUT_FORMAT_KEY,$args)) {
165  $possibleOutputFormat = trim($args[self::OUTPUT_FORMAT_KEY]);
166  if (in_array($possibleOutputFormat, explode(',',self::AVAILABLE_OUTPUT_FORMATS))) {
167  $this->outputFormat = $possibleOutputFormat;
168  }
169  }
170  $this->computeUri($uploadId);
171 
172  $contents = $this->renderPackage($uploadId, $groupId);
173 
174  $additionalUploadIds = array_key_exists(self::UPLOAD_ADDS,$args) ? explode(',',$args[self::UPLOAD_ADDS]) : array();
175  $packageIds = array($uploadId);
176  foreach ($additionalUploadIds as $additionalId) {
177  $contents .= $this->renderPackage($additionalId, $groupId);
178  $packageIds[] = $additionalId;
179  }
180 
181  $this->writeReport($contents, $packageIds, $uploadId);
182  return true;
183  }
184 
185  protected function getTemplateFile($partname)
186  {
187  $prefix = $this->outputFormat . "-";
188  $postfix = ".twig";
189  $postfix = ".xml" . $postfix;
190  return $prefix . $partname . $postfix;
191  }
192 
193  protected function getUri($fileBase)
194  {
195  if (count($this->additionalUploads) > 0) {
196  $fileName = $fileBase . "multifile" . "_" . strtoupper($this->outputFormat);
197  } else {
198  $fileName = $fileBase. strtoupper($this->outputFormat)."_".$this->packageName;
199  }
200 
201  return $fileName .".xml";
202  }
203 
204  protected function renderPackage($uploadId, $groupId)
205  {
206  $this->heartbeat(0);
207 
208  $otherStatement = $this->otherGetter->getReportData($uploadId);
209  $this->heartbeat(empty($otherStatement) ? 0 : count($otherStatement));
210 
211  if (!empty($otherStatement['ri_clixmlcolumns'])) {
212  $clixmlColumns = json_decode($otherStatement['ri_clixmlcolumns'], true);
213  } else {
214  $clixmlColumns = UploadDao::CLIXML_REPORT_HEADINGS;
215  }
216 
217  $licenses = $this->licenseClearedGetter->getCleared($uploadId, $this, $groupId, true, "license", false);
218  $this->heartbeat(empty($licenses) ? 0 : count($licenses["statements"]));
219 
220  $licensesMain = $this->licenseMainGetter->getCleared($uploadId, $this, $groupId, true, null, false);
221  $this->heartbeat(empty($licensesMain) ? 0 : count($licensesMain["statements"]));
222 
223  if (array_values($clixmlColumns['irrelevantfilesclixml'])[0]) {
224  $licensesIrre = $this->licenseIrrelevantGetter->getCleared($uploadId, $this, $groupId, true, null, false);
225  $irreComments = $this->licenseIrrelevantGetterComments->getCleared($uploadId, $this, $groupId, true, null, false);
226  } else {
227  $licensesIrre = array("statements" => array());
228  $irreComments = array("statements" => array());
229  }
230  $this->heartbeat(empty($licensesIrre) ? 0 : count($licensesIrre["statements"]));
231  $this->heartbeat(empty($irreComments) ? 0 : count($irreComments["statements"]));
232 
233  if (array_values($clixmlColumns['dnufilesclixml'])[0]) {
234  $licensesDNU = $this->licenseDNUGetter->getCleared($uploadId, $this, $groupId, true, null, false);
235  $licensesDNUComment = $this->licenseDNUCommentGetter->getCleared($uploadId, $this, $groupId, true, null, false);
236  } else {
237  $licensesDNU = array("statements" => array());
238  $licensesDNUComment = array("statements" => array());
239  }
240  $this->heartbeat(empty($licensesDNU) ? 0 : count($licensesDNU["statements"]));
241  $this->heartbeat(empty($licensesDNUComment) ? 0 : count($licensesDNUComment["statements"]));
242 
243  if (array_values($clixmlColumns['copyrightsclixml'])[0]) {
244  $copyrights = $this->cpClearedGetter->getCleared($uploadId, $this, $groupId, true, "copyright", false);
245  } else {
246  $copyrights = array("statements" => array());
247  }
248  $this->heartbeat(empty($copyrights["statements"]) ? 0 : count($copyrights["statements"]));
249 
250  if (array_values($clixmlColumns['exportrestrictionsclixml'])[0]) {
251  $ecc = $this->eccClearedGetter->getCleared($uploadId, $this, $groupId, true, "ecc", false);
252  } else {
253  $ecc = array("statements" => array());
254  }
255  $this->heartbeat(empty($ecc) ? 0 : count($ecc["statements"]));
256 
257  if (array_values($clixmlColumns['intellectualPropertyclixml'])[0]) {
258  $ipra = $this->ipraClearedGetter->getCleared($uploadId, $this, $groupId, true, "ipra", false);
259  } else {
260  $ipra = array("statements" => array());
261  }
262  $this->heartbeat(empty($ipra) ? 0 : count($ipra["statements"]));
263 
264  if (array_values($clixmlColumns['notesclixml'])[0]) {
265  $notes = htmlspecialchars($otherStatement['ri_ga_additional'], ENT_DISALLOWED);
266  } else {
267  $notes = "";
268  }
269 
270  $countAcknowledgement = 0;
271  $includeAcknowledgements = array_values($clixmlColumns['acknowledgementsclixml'])[0];
272  $licenses["statements"] = $this->addLicenseNames($licenses["statements"]);
273  $licensesWithAcknowledgement = $this->removeDuplicateAcknowledgements(
274  $licenses["statements"], $countAcknowledgement, $includeAcknowledgements);
275 
276  if (array_values($clixmlColumns['allobligations'])[0]) {
277  $obligations = $this->obligationsGetter->getObligations(
278  $licenses['statements'], $licensesMain['statements'], $uploadId, $groupId)[0];
279  $obligations = array_values($obligations);
280  } else {
281  $obligations = array();
282  }
283 
284  if (array_values($clixmlColumns['mainlicensesclixml'])[0]) {
285  $mainLicenses = $licensesMain["statements"];
286  } else {
287  $mainLicenses = array();
288  }
289  $componentHash = $this->uploadDao->getUploadHashes($uploadId);
290  $contents = array(
291  "licensesMain" => $mainLicenses,
292  "licenses" => $licensesWithAcknowledgement,
293  "obligations" => $obligations,
294  "copyrights" => $copyrights["statements"],
295  "ecc" => $ecc["statements"],
296  "ipra" => $ipra["statements"],
297  "licensesIrre" => $licensesIrre["statements"],
298  "irreComments" => $irreComments["statements"],
299  "licensesDNU" => $licensesDNU["statements"],
300  "licensesDNUComment" => $licensesDNUComment["statements"],
301  "countAcknowledgement" => $countAcknowledgement
302  );
303  $contents = $this->reArrangeMainLic($contents, $includeAcknowledgements);
304  $contents = $this->reArrangeContent($contents);
305  $fileOperations = array(
306  "licensepath" => array_values($clixmlColumns['licensepath'])[0],
307  "licensehash" => array_values($clixmlColumns['licensehash'])[0],
308  "copyrightpath" => array_values($clixmlColumns['copyrightpath'])[0],
309  "copyrighthash" => array_values($clixmlColumns['copyrighthash'])[0],
310  "eccpath" => array_values($clixmlColumns['eccpath'])[0],
311  "ecchash" => array_values($clixmlColumns['ecchash'])[0],
312  "iprapath" => array_values($clixmlColumns['iprapath'])[0],
313  "iprahash" => array_values($clixmlColumns['iprahash'])[0]
314  );
315  list($generalInformation, $assessmentSummary) = $this->getReportSummary($uploadId);
316  $generalInformation['componentHash'] = $componentHash['sha1'];
317  return $this->renderString($this->getTemplateFile('file'),array(
318  'documentName' => $this->packageName,
319  'version' => "1.6",
320  'uri' => $this->uri,
321  'userName' => $this->container->get('dao.user')->getUserName($this->userId),
322  'organisation' => '',
323  'componentHash' => strtolower($componentHash['sha1']),
324  'contents' => $contents,
325  'commentAdditionalNotes' => $notes,
326  'externalIdLink' => htmlspecialchars($otherStatement['ri_sw360_link']),
327  'generalInformation' => $generalInformation,
328  'assessmentSummary' => $assessmentSummary,
329  'fileOperations' => $fileOperations
330  ));
331  }
332 
333  protected function removeDuplicateAcknowledgements($licenses, &$countAcknowledgement, $includeAcknowledgements)
334  {
335  if (empty($licenses)) {
336  return $licenses;
337  }
338 
339  foreach ($licenses as $ackKey => $ackValue) {
340  if (!$includeAcknowledgements) {
341  $licenses[$ackKey]['acknowledgement'] = null;
342  } else if (isset($ackValue['acknowledgement'])) {
343  $licenses[$ackKey]['acknowledgement'] = array_unique(array_filter($ackValue['acknowledgement']));
344  $countAcknowledgement += count($licenses[$ackKey]['acknowledgement']);
345  }
346  }
347  return $licenses;
348  }
349 
350  protected function riskMapping($licenseContent)
351  {
352  foreach ($licenseContent as $riskKey => $riskValue) {
353  if (!array_key_exists('risk', $riskValue)) {
354  $riskValue['risk'] = 0;
355  }
356  if ($riskValue['risk'] == '2' || $riskValue['risk'] == '3') {
357  $licenseContent[$riskKey]['risk'] = 'otheryellow';
358  } else if ($riskValue['risk'] == '4' || $riskValue['risk'] == '5') {
359  $licenseContent[$riskKey]['risk'] = 'otherred';
360  } else {
361  $licenseContent[$riskKey]['risk'] = 'otherwhite';
362  }
363  }
364  return $licenseContent;
365  }
366 
367  protected function reArrangeMainLic($contents, $includeAcknowledgements)
368  {
369  $mainlic = array();
370  $lenTotalLics = count($contents["licenses"]);
371  // both of this variables have same value but used for different operations
372  $lenMainLics = count($contents["licensesMain"]);
373  for ($i=0; $i<$lenMainLics; $i++) {
374  $count = 0 ;
375  for ($j=0; $j<$lenTotalLics; $j++) {
376  if (!strcmp($contents["licenses"][$j]["content"], $contents["licensesMain"][$i]["content"])) {
377  $count = 1;
378  $mainlic[] = $contents["licenses"][$j];
379  unset($contents["licenses"][$j]);
380  }
381  }
382  if ($count != 1) {
383  $mainlic[] = $contents["licensesMain"][$i];
384  }
385  unset($contents["licensesMain"][$i]);
386  }
387  $contents["licensesMain"] = $mainlic;
388 
389  $lenMainLicenses=count($contents["licensesMain"]);
390  for ($i=0; $i<$lenMainLicenses; $i++) {
391  $contents["licensesMain"][$i]["contentMain"] = $contents["licensesMain"][$i]["content"];
392  $contents["licensesMain"][$i]["nameMain"] = $contents["licensesMain"][$i]["name"];
393  $contents["licensesMain"][$i]["textMain"] = $contents["licensesMain"][$i]["text"];
394  $contents["licensesMain"][$i]["riskMain"] = $contents["licensesMain"][$i]["risk"];
395  if (array_key_exists('acknowledgement', $contents["licensesMain"][$i])) {
396  if ($includeAcknowledgements) {
397  $contents["licensesMain"][$i]["acknowledgementMain"] = $contents["licensesMain"][$i]["acknowledgement"];
398  }
399  unset($contents["licensesMain"][$i]["acknowledgement"]);
400  }
401  unset($contents["licensesMain"][$i]["content"]);
402  unset($contents["licensesMain"][$i]["text"]);
403  unset($contents["licensesMain"][$i]["risk"]);
404  }
405  return $contents;
406  }
407 
408  protected function reArrangeContent($contents)
409  {
410  $contents['licensesMain'] = $this->riskMapping($contents['licensesMain']);
411  $contents['licenses'] = $this->riskMapping($contents['licenses']);
412 
413  $contents["obligations"] = array_map(function($changeKey) {
414  return array(
415  'obliText' => $changeKey['text'],
416  'topic' => $changeKey['topic'],
417  'license' => $changeKey['license']
418  );
419  }, $contents["obligations"]);
420 
421  $contents["copyrights"] = array_map(function($changeKey) {
422  $content = htmlspecialchars_decode($changeKey['content']);
423  $content = str_replace("]]>", "]]&gt;", $content);
424  $comments = htmlspecialchars_decode($changeKey['comments']);
425  $comments = str_replace("]]>", "]]&gt;", $comments);
426  return array(
427  'contentCopy' => $content,
428  'comments' => $comments,
429  'files' => $changeKey['files'],
430  'hash' => $changeKey['hash']
431  );
432  }, $contents["copyrights"]);
433 
434  $contents["ecc"] = array_map(function($changeKey) {
435  $content = htmlspecialchars_decode($changeKey['content']);
436  $content = str_replace("]]>", "]]&gt;", $content);
437  $comments = htmlspecialchars_decode($changeKey['comments']);
438  $comments = str_replace("]]>", "]]&gt;", $comments);
439  return array(
440  'contentEcc' => $content,
441  'commentsEcc' => $comments,
442  'files' => $changeKey['files'],
443  'hash' => $changeKey['hash']
444  );
445  }, $contents["ecc"]);
446 
447  $contents["ipra"] = array_map(function($changeKey) {
448  $content = htmlspecialchars_decode($changeKey['content']);
449  $content = str_replace("]]>", "]]&gt;", $content);
450  $comments = htmlspecialchars_decode($changeKey['comments']);
451  $comments = str_replace("]]>", "]]&gt;", $comments);
452  return array(
453  'contentIpra' => $content,
454  'commentsIpra' => $comments,
455  'files' => $changeKey['files'],
456  'hash' => $changeKey['hash']
457  );
458  }, $contents["ipra"]);
459 
460  $contents["irreComments"] = array_map(function($changeKey) {
461  return array(
462  'contentIrre' => $changeKey['content'],
463  'textIrre' => $changeKey['text']
464  );
465  }, $contents["irreComments"]);
466 
467  $contents["licensesIrre"] = array_map(function($changeKey) {
468  return array(
469  'filesIrre' => $changeKey['fullPath']
470  );
471  }, $contents["licensesIrre"]);
472 
473  $contents["licensesDNUComment"] = array_map(function($changeKey) {
474  return array(
475  'contentDNU' => $changeKey['content'],
476  'textDNU' => $changeKey['text']
477  );
478  }, $contents["licensesDNUComment"]);
479 
480  $contents["licensesDNU"] = array_map(function($changeKey) {
481  return array(
482  'filesDNU' => $changeKey['fullPath']
483  );
484  }, $contents["licensesDNU"]);
485 
486  return $contents;
487  }
488 
489  protected function computeUri($uploadId)
490  {
491  global $SysConf;
492  $upload = $this->uploadDao->getUpload($uploadId);
493  $this->packageName = $upload->getFilename();
494 
495  $fileBase = $SysConf['FOSSOLOGY']['path']."/report/";
496 
497  $this->uri = $this->getUri($fileBase);
498  }
499 
500  protected function writeReport($contents, $packageIds, $uploadId)
501  {
502  $fileBase = dirname($this->uri);
503 
504  if (!is_dir($fileBase)) {
505  mkdir($fileBase, 0777, true);
506  }
507  umask(0133);
508 
509  $message = $this->renderString($this->getTemplateFile('document'),
510  array('content' => $contents));
511 
512  // To ensure the file is valid, replace any non-printable characters with a question mark.
513  // 'Non-printable' is ASCII < 0x20 (excluding \r, \n and tab) and 0x7F - 0x9F.
514  $message = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/u','?',$message);
515  file_put_contents($this->uri, $message);
516  $this->updateReportTable($uploadId, $this->jobId, $this->uri);
517  }
518 
519  protected function updateReportTable($uploadId, $jobId, $fileName)
520  {
521  $this->reportutils->updateOrInsertReportgenEntry($uploadId, $jobId, $fileName);
522  }
523 
529  protected function renderString($templateName, $vars)
530  {
531  return $this->renderer->load($templateName)->render($vars);
532  }
533 
541  private function getReportSummary($uploadId)
542  {
543  global $SysConf;
544  $row = $this->uploadDao->getReportInfo($uploadId);
545 
546  $review = htmlspecialchars($row['ri_reviewed']);
547  if ($review == 'NA') {
548  $review = '';
549  }
550  $critical = 'None';
551  $dependency = 'None';
552  $ecc = 'None';
553  $usage = 'None';
554  if (!empty($row['ri_ga_checkbox_selection'])) {
555  $listURCheckbox = explode(',', $row['ri_ga_checkbox_selection']);
556  if ($listURCheckbox[0] == 'checked') {
557  $critical = 'None';
558  }
559  if ($listURCheckbox[1] == 'checked') {
560  $critical = 'Found';
561  }
562  if ($listURCheckbox[2] == 'checked') {
563  $dependency = 'None';
564  }
565  if ($listURCheckbox[3] == 'checked') {
566  $dependency = 'SourceDependenciesFound';
567  }
568  if ($listURCheckbox[4] == 'checked') {
569  $dependency = 'BinaryDependenciesFound';
570  }
571  if ($listURCheckbox[5] == 'checked') {
572  $ecc = 'None';
573  }
574  if ($listURCheckbox[6] == 'checked') {
575  $ecc = 'Found';
576  }
577  if ($listURCheckbox[7] == 'checked') {
578  $usage = 'None';
579  }
580  if ($listURCheckbox[8] == 'checked') {
581  $usage = 'Found';
582  }
583  }
584  $componentType = $row['ri_component_type'];
585  if (!empty($componentType)) {
586  $componentType = ComponentType::TYPE_MAP[$componentType];
587  } else {
589  }
590  $componentId = $row['ri_component_id'];
591  if (empty($componentId) || $componentId == "NA") {
592  $componentId = "";
593  }
594 
595  $parentItem = $this->uploadDao->getUploadParent($uploadId);
596 
597  $uploadLink = $SysConf['SYSCONFIG']['FOSSologyURL'];
598  if (substr($uploadLink, 0, 4) !== "http") {
599  $uploadLink = "http://" . $uploadLink;
600  }
601  $uploadLink .= "?mod=browse&upload=$uploadId&item=$parentItem";
602 
603  return [[
604  'reportId' => uuid_create(UUID_TYPE_TIME),
605  'reviewedBy' => $review,
606  'componentName' => htmlspecialchars($row['ri_component']),
607  'community' => htmlspecialchars($row['ri_community']),
608  'version' => htmlspecialchars($row['ri_version']),
609  'componentHash' => '',
610  'componentReleaseDate' => htmlspecialchars($row['ri_release_date']),
611  'linkComponentManagement' => htmlspecialchars($row['ri_sw360_link']),
612  'linkScanTool' => $uploadLink,
613  'componentType' => htmlspecialchars($componentType),
614  'componentId' => htmlspecialchars($componentId)
615  ], [
616  'generalAssessment' => $row['ri_general_assesment'],
617  'criticalFilesFound' => $critical,
618  'dependencyNotes' => $dependency,
619  'exportRestrictionsFound' => $ecc,
620  'usageRestrictionsFound' => $usage,
621  'additionalNotes' => $row['ri_ga_additional']
622  ]];
623  }
624 
630  private function addLicenseNames($licenses)
631  {
632  $statementsWithNames = [];
633  foreach ($licenses as $license) {
634  $allLicenseCols = $this->licenseDao->getLicenseById($license["licenseId"],
635  $this->groupId);
636  $license["name"] = $allLicenseCols->getShortName();
637  $statementsWithNames[] = $license;
638  }
639  return $statementsWithNames;
640  }
641 }
642 
643 $agent = new CliXml();
644 $agent->scheduler_connect();
645 $agent->run_scheduler_event_loop();
646 $agent->scheduler_disconnect(0);
addLicenseNames($licenses)
Definition: clixml.php:630
preWorkOnArgsFlp($args, $key1, $key2)
Definition: clixml.php:127
getReportSummary($uploadId)
Definition: clixml.php:541
renderString($templateName, $vars)
Definition: clixml.php:529
processUploadId($uploadId)
Given an upload ID, process the items in it.
Definition: clixml.php:158
Structure of an Agent with all required parameters.
Definition: Agent.php:41
heartbeat($newProcessed)
Send hear beat to the scheduler.
Definition: Agent.php:203
char * trim(char *ptext)
Trimming whitespace.
Definition: fossconfig.c:690
int jobId
The id of the job.
fo_dbManager * dbManager
fo_dbManager object
Definition: process.c:16
FUNCTION char * strtoupper(char *s)
Helper function to upper case a string.
Definition: utils.c:39