basedir); // 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 an extension manifest if (!$xml) { 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; } // Load DTD-like data for this extension type $jsonFilename = __DIR__ . '/xmlmanifest/dtd_' . $type . '.json'; if (!is_file($jsonFilename)) { 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')); } switch ($type) { case 'language': case 'module': case 'template': // Check 'client' attribute is "site" or "administrator" (for language/module/template only) $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)); } if ($type === 'module') { // Either or "module" attribute (once only) should be present $elements = $this->collectElements($xml->files, $type); if (count($elements) >= 2) { $this->report->addWarning($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MULTIPLE_ATTRIBUTES', 'module')); } if (isset($xml->element)) { $element = (string) $xml->element; if (count($elements) && $elements[0] !== $element) { $this->report->addWarning($file, JText::_('COM_JEDCHECKER_MANIFEST_MODULE_ELEMENT_MISMATCH')); } } else { if (count($elements) === 0) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_ELEMENT_ATTRIBUTE', 'module')); } } } break; case 'plugin': // "plugin" attribute (once only) should be present $elements = $this->collectElements($xml->files, $type); if (count($elements) >= 2) { $this->report->addWarning($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MULTIPLE_ATTRIBUTES', 'plugin')); } if (count($elements) === 0) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_ELEMENT_ATTRIBUTE', 'plugin')); } break; case 'package': // Check type-specific attributes foreach ($xml->files->children() as $item) { if (!isset($item['type'])) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_ATTRIBUTE', $item->getName(), 'type')); } if (!isset($item['id'])) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_ATTRIBUTE', $item->getName(), 'id')); } switch ((string) $item['type']) { case 'plugin': if (!isset($item['group'])) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_ATTRIBUTE', $item->getName(), 'group')); } break; case 'language': case 'module': case 'template': $client = (string) $item['client']; if (!isset($item['client'])) { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_ATTRIBUTE', $item->getName(), 'client')); } elseif ($client !== 'site' && $client !== 'administrator') { $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_ATTRIBUTE_VALUE', $item->getName(), 'client', $client)); } break; case 'component': case 'file': case 'library': break; default: $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_TYPE', $item['type'])); } } } $data = json_decode(file_get_contents($jsonFilename), true); $this->DTDNodeRules = $data['nodes']; $this->DTDAttrRules = $data['attributes']; $this->errors = array(); $this->warnings = array(); $this->notices = 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->notices)) { $this->report->addNotice($file, implode('
', $this->notices)); } // All checks passed. Return true return true; } /** * @param SimpleXMLElement $node XML node object * @param string $ruleset ruleset name in the DTD array * * @return void */ protected function validateXml($node, $ruleset) { // Get node name $name = $node->getName(); // Check attributes $DTDattributes = isset($this->DTDAttrRules[$ruleset]) ? $this->DTDAttrRules[$ruleset] : array(); if (count($DTDattributes) === 0) { // No known attributes for this node foreach ($node->attributes() as $attr) { $this->notices[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_ATTRIBUTE', $name, (string) $attr->getName()); } } elseif ($DTDattributes[0] !== '*') // Skip node with arbitrary attributes (e.g. "field") { foreach ($node->attributes() as $attr) { $attrName = (string) $attr->getName(); if (!in_array($attrName, $DTDattributes, true)) { // The node has unknown attribute $this->notices[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_ATTRIBUTE', $name, $attrName); } } } // Check children nodes $DTDchildRules = isset($this->DTDNodeRules[$ruleset]) ? $this->DTDNodeRules[$ruleset] : array(); // Child node name to ruleset name mapping $DTDchildToRule = array(); if (count($DTDchildRules) === 0) { // No known children for this node if ($node->count() > 0) { $this->notices[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_CHILDREN', $name); } } elseif (!isset($DTDchildRules['*'])) // Skip node with arbitrary children { // 1) check required single elements foreach ($DTDchildRules as $childRuleset => $mode) { $child = $childRuleset; if (strpos($child, ':') !== false) { // Split ruleset name into a prefix and the child node name list ($prefix, $child) = explode(':', $child, 2); } // Populate node-to-ruleset mapping $DTDchildToRule[$child] = $childRuleset; $count = $node->$child->count(); switch ($mode) { case '!': if ($count === 0) { // The node doesn't contain required child element $this->errors[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_REQUIRED', $name, $child); } elseif ($count > 1) { // The node contains multiple child elements when single only is expected $this->errors[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MULTIPLE_FOUND', $name, $child); } break; case '=': if ($count === 0) { // The node doesn't contain optional child element $this->notices[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MISSED_OPTIONAL', $name, $child); } elseif ($count > 1) { // The node contains multiple child elements when single only is expected $this->warnings[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MULTIPLE_FOUND', $name, $child); } break; } } // 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($DTDchildToRule[$child])) { // The node contains unknown child element $this->notices[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_CHILD', $name, $child); } else { if ($DTDchildRules[$DTDchildToRule[$child]] === '?' && $node->$child->count() > 1) { // The node contains multiple child elements when single only is expected $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->notices[] = 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($DTDchildToRule[$childName])) { $this->validateXml($child, $DTDchildToRule[$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); } } } } /** * Collect values of $type attribute from all children * @param SimpleXMLElement $node XML node * @param string $type Extension's type (plugin or module) * * @return array List of found attributes */ protected function collectElements($node, $type) { $elements = array(); if (isset($node)) { foreach ($node->children() as $child) { if (isset($child[$type])) { $elements[] = (string) $child[$type]; } } } return $elements; } }