basedir, '\.xml$', true, true); // Iterate through all the xml files foreach ($files as $file) { // Try to check the file $this->find($file); } } /** * Reads a file and validate XML manifest * * @param string $file - The path to the file * * @return boolean True if the manifest file was found, otherwise False. */ protected function find($file) { $xml = simplexml_load_file($file); // Failed to parse the xml file. // Assume that this is not a extension manifest if (!$xml) { return false; } // Check if this is an extension manifest if ($xml->getName() !== 'extension') { return false; } // Check extension type $type = (string) $xml['type']; if (!in_array($type, $this->joomlaTypes, true)) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_TYPE', $type)); return true; } // JED allows components, modules, plugins, and packages (as a container) only if (!in_array($type, $this->jedTypes, true)) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_TYPE_NOT_ACCEPTED', $type)); } // Load DTD-like data for this extension type $json_filename = __DIR__ . '/xmlmanifest/dtd_' . $type . '.json'; if (!is_file($json_filename)) { return true; } // warn if method="upgrade" attribute is not found if ((string) $xml['method'] !== 'upgrade') { $this->report->addWarning($file, JText::_('COM_JEDCHECKER_MANIFEST_MISSED_METHOD_UPGRADE')); } // check 'client' attribute is "site" or "administrator" (for module/template only) if ($type === 'module' || $type === 'template') { $client = (string) $xml['client']; if (!isset($xml['client'])) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_ATTRIBUTE', $xml->getName(), 'client')); } elseif ($client !== 'site' && $client !== 'administrator') { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_ATTRIBUTE_VALUE', $xml->getName(), 'client', $client)); } } $data = json_decode(file_get_contents($json_filename), true); $this->DTDNodeRules = $data['nodes']; $this->DTDAttrRules = $data['attributes']; $this->errors = array(); $this->warnings = array(); $this->infos = array(); // Validate manifest $this->validateXml($xml, 'extension'); if (count($this->errors)) { $this->report->addError($file, implode('
', $this->errors)); } if (count($this->warnings)) { $this->report->addWarning($file, implode('
', $this->warnings)); } if (count($this->infos)) { $this->report->addInfo($file, implode('
', $this->infos)); } // All checks passed. Return true return true; } /** * @param SimpleXMLElement $node XML node object * @param string $name XML node name * * @return void */ protected function validateXml($node, $name) { // Check attributes $DTDattributes = isset($this->DTDAttrRules[$name]) ? $this->DTDAttrRules[$name] : array(); if (isset($DTDattributes[0]) && $DTDattributes[0] !== '*') { foreach ($node->attributes() as $attr) { $attrName = (string) $attr->getName(); if (!in_array($attrName, $DTDattributes, true)) { // The node has unknown attribute $this->infos[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_ATTRIBUTE', $name, $attrName); } } } // Check children nodes if (!isset($this->DTDNodeRules[$name])) { // No children if ($node->count() > 0) { $this->infos[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_CHILDREN', $name); } } elseif (!isset($this->DTDNodeRules[$name]['*'])) { $DTDchildren = $this->DTDNodeRules[$name]; // 1) check required single elements foreach ($DTDchildren as $child => $mode) { $count = $node->$child->count(); switch ($mode) { case '!': $errors =& $this->errors; break; case '=': $errors =& $this->warnings; break; default: continue 2; } if ($count === 0) { $errors[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_REQUIRED', $name, $child); } elseif ($count > 1) { $errors[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MULTIPLE_FOUND', $name, $child); } unset($errors); } // 2) check unknown/multiple elements // Collect unique child node names $childNames = array(); foreach ($node as $child) { $childNames[$child->getName()] = 1; } $childNames = array_keys($childNames); foreach ($childNames as $child) { if (!isset($DTDchildren[$child])) { $this->infos[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_CHILD', $name, $child); } else { if ($DTDchildren[$child] === '?' && $node->$child->count() > 1) { $this->errors[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MULTIPLE_FOUND', $name, $child); } } } // 3) check empty elements foreach ($node as $child) { if ($child->count() === 0 && $child->attributes()->count() === 0 && (string) $child === '') { $this->infos[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_EMPTY_CHILD', $child->getName()); } } } // Extra checks (if exist) $method = 'validateXml' . $name; if (method_exists($this, $method)) { $this->$method($node); } // Recursion foreach ($node as $child) { $childName = $child->getName(); if (isset($this->DTDNodeRules[$childName])) { $this->validateXml($child, $childName); } } } /** * Extra check for menu nodes * @param SimpleXMLElement $node XML node * * @return void */ protected function validateXmlMenu($node) { if (isset($node['link'])) { // The "link" attribute overrides any other link-related attributes (warn if they present) $skipAttrs = array('act', 'controller', 'layout', 'sub', 'task', 'view'); foreach ($node->attributes() as $attr) { $attrName = $attr->getName(); if (in_array($attrName, $skipAttrs, true)) { $this->warnings[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MENU_UNUSED_ATTRIBUTE', $attrName); } } } } }