diff --git a/administrator/components/com_jedchecker/language/en-GB/en-GB.com_jedchecker.ini b/administrator/components/com_jedchecker/language/en-GB/en-GB.com_jedchecker.ini index 7231b32..ab71533 100644 --- a/administrator/components/com_jedchecker/language/en-GB/en-GB.com_jedchecker.ini +++ b/administrator/components/com_jedchecker/language/en-GB/en-GB.com_jedchecker.ini @@ -77,4 +77,14 @@ COM_JEDCHECKER_ERROR_XML_UPDATE_SERVER_LINK_NOT_FOUND="Update Server link not fo COM_JEDCHECKER_INFO_XML_UPDATE_SERVER_LINK="The Update Server link in this XML file is: %s" COM_JEDCHECKER_DELETE_FAILED="Can't delete temporary folder" COM_JEDCHECKER_DELETE_SUCCESS="Temporary folder deleted!" -COM_JEDCHECKER_EMPTY_UPLOAD_FIELD="Please, select a zipped file to be uploaded" \ No newline at end of file +COM_JEDCHECKER_EMPTY_UPLOAD_FIELD="Please, select a zipped file to be uploaded" +COM_JEDCHECKER_MANIFEST="XML Manifest" +COM_JEDCHECKER_MANIFEST_DESC="Validation of extension's XML manifest file" +COM_JEDCHECKER_MANIFEST_UNKNOWN_TYPE="Unknown extension type: %s" +COM_JEDCHECKER_MANIFEST_TYPE_NOT_ACCEPTED="Extension type '%s' is not accepted by JED" +COM_JEDCHECKER_MANIFEST_UNKNOWN_ATTRIBUTE="Node '%1$s' has unknown attribute '%2$s'" +COM_JEDCHECKER_MANIFEST_UNKNOWN_CHILDREN="Node '%s' has unknown child element" +COM_JEDCHECKER_MANIFEST_MISSED_REQUIRED="Node '%1$s' doesn't contain required '%2$s' element" +COM_JEDCHECKER_MANIFEST_MULTIPLE_FOUND="Node '%1$s' contains multiple '%2$s' elements" +COM_JEDCHECKER_MANIFEST_UNKNOWN_CHILD="Node '%1$s' contains unknown '%2$s' element" +COM_JEDCHECKER_MANIFEST_MENU_UNUSED_ATTRIBUTE="Menu item attribute '%s' is not used with 'link' attribute" \ No newline at end of file diff --git a/administrator/components/com_jedchecker/libraries/rules/xmlmanifest.php b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest.php new file mode 100644 index 0000000..328605d --- /dev/null +++ b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest.php @@ -0,0 +1,285 @@ + + * eaxs + * Denis Ryabov + * + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die('Restricted access'); + + +// Include the rule base class +require_once JPATH_COMPONENT_ADMINISTRATOR . '/models/rule.php'; + + +/** + * class JedcheckerRulesXMLManifest + * + * This class validates all xml manifestes + * + * @since 1.0 + */ +class JedcheckerRulesXMLManifest extends JEDcheckerRule +{ + /** + * The formal ID of this rule. For example: SE1. + * + * @var string + */ + protected $id = 'MANIFEST'; + + /** + * The title or caption of this rule. + * + * @var string + */ + protected $title = 'COM_JEDCHECKER_MANIFEST'; + + /** + * The description of this rule. + * + * @var string + */ + protected $description = 'COM_JEDCHECKER_MANIFEST_DESC'; + + /** + * List of errors. + * + * @var string[] + */ + protected $errors; + + /** + * List of warnings. + * + * @var string[] + */ + protected $warnings; + + /** + * Rules for XML nodes + * ? - single, optional + * = - single, required, warning if missed + * ! - single, required, error if missed + * * - multiple, optional + * @var array + */ + protected $DTDNodeRules; + + /** + * Rules for attributes + * (list of allowed attributes) + * @var array + */ + protected $DTDAttrRules; + + protected $types = array( + 'component', 'file', 'language', 'library', + 'module', 'package', 'plugin', 'template' + ); + + /** + * Initiates the search and check + * + * @return void + */ + public function check() + { + // Find all XML files of the extension + $files = JFolder::files($this->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 = JFactory::getXml($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->types, true)) + { + $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_TYPE', $type)); + return true; + } + + // load DTD-like data for this extension type + $json_filename = __DIR__ . '/xmlmanifest_' . $type . '.json'; + if (!is_file($json_filename)) + { + $this->report->addError($file, JText::sprintf('COM_JEDCHECKER_MANIFEST_TYPE_NOT_ACCEPTED', $type)); + return true; + } + $data = json_decode(file_get_contents($json_filename), true); + $this->DTDNodeRules = $data['nodes']; + $this->DTDAttrRules = $data['attributes']; + + $this->errors = array(); + $this->warnings = 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)); + } + + // All checks passed. Return true + return true; + } + + /** + * @param JXMLElement $node + * @param string $name + */ + protected function validateXml($node, $name) + { + // Check attributes + $DTDattributes = isset($this->DTDAttrRules[$name]) ? $this->DTDAttrRules[$name] : array(); + foreach ($node->attributes() as $attr) + { + $attr_name = (string)$attr->getName(); + if (!in_array($attr_name, $DTDattributes, true)) + { + $this->warnings[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_ATTRIBUTE', $name, $attr_name); + } + } + + // Check children nodes + if (!isset($this->DTDNodeRules[$name])) + { + // No children + if ($node->count() > 0) + { + $this->warnings[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_UNKNOWN_CHILDREN', $name); + } + } else { + $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 + $child_names = array(); + foreach ($node as $child) + { + $child_names[$child->getName()] = 1; + } + $child_names = array_keys($child_names); + + foreach ($child_names as $child) + { + if (!isset($DTDchildren[$child])) + { + $this->warnings[] = 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); + } + } + } + } + + // Extra checks (if exist) + $method = 'validateXml' . $name; + if (method_exists($this, $method)) + { + $this->$method($node); + } + + // Recursion + foreach ($node as $child) + { + $child_name = $child->getName(); + if (isset($this->DTDNodeRules[$child_name])) { + $this->validateXml($child, $child_name); + } + } + } + + /** + * Extra check for menu nodes + * @param JXMLElement $node + */ + protected function validateXmlMenu($node) + { + if (isset($node['link'])) + { + $skip_attrs = array('act', 'controller', 'layout', 'sub', 'task', 'view'); + foreach ($node->attributes() as $attr) + { + $attr_name = $attr->getName(); + if (in_array($attr_name, $skip_attrs, true)) + { + $this->warnings[] = JText::sprintf('COM_JEDCHECKER_MANIFEST_MENU_UNUSED_ATTRIBUTE', $attr_name); + } + } + } + } +} diff --git a/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_component.json b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_component.json new file mode 100644 index 0000000..6a6d949 --- /dev/null +++ b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_component.json @@ -0,0 +1,129 @@ +{ + "nodes": { + "extension": { + "name": "!", + "element": "?", + "creationDate": "=", + "author": "=", + "authorEmail": "=", + "authorUrl": "=", + "copyright": "=", + "version": "!", + "description": "=", + "license": "!", + "scriptfile": "?", + "install": "?", + "update": "?", + "uninstall": "?", + "files": "?", + "languages": "?", + "media": "?", + "administration": "?", + "updateservers": "!", + "config": "?", + "dlid": "?" + }, + "administration": { + "menu": "=", + "submenu": "?", + "files": "=", + "languages": "=", + "media": "?" + }, + "files": { + "filename": "*", + "folder": "*" + }, + "languages": { + "language": "*" + }, + "media": { + "filename": "*", + "folder": "*" + }, + "submenu": { + "menu": "*" + }, + "install": { + "sql": "*" + }, + "update": { + "sql": "*", + "schemas": "*" + }, + "uninstall": { + "sql": "*" + }, + "sql": { + "file": "*" + }, + "schemas": { + "schemapath": "*" + }, + "updateservers": { + "server": "*" + }, + "config": { + "fields": "!" + }, + "fields": { + "fieldset": "+" + }, + "fieldset": { + "???": "*" + } + }, + "attributes": { + "extension": [ + "client", + "method", + "overwrite", + "type", + "version" + ], + "files": [ + "folder" + ], + "languages": [ + "folder" + ], + "language": [ + "client", + "tag" + ], + "media": [ + "destination", + "folder" + ], + "menu": [ + "act", + "controller", + "hidden", + "img", + "layout", + "link", + "sub", + "task", + "view" + ], + "file": [ + "charset", + "driver" + ], + "server": [ + "name", + "priority", + "type" + ], + "params": [ + "addParameterDir" + ], + "param": [ + "addParameterDir" + ], + "dlid": [ + "prefix", + "suffix" + ] + } +} \ No newline at end of file diff --git a/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_module.json b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_module.json new file mode 100644 index 0000000..8747f30 --- /dev/null +++ b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_module.json @@ -0,0 +1,117 @@ +{ + "nodes": { + "extension": { + "name": "!", + "element": "?", + "creationDate": "=", + "author": "=", + "authorEmail": "=", + "authorUrl": "=", + "copyright": "=", + "version": "!", + "description": "=", + "license": "!", + "scriptfile": "?", + "install": "?", + "update": "?", + "uninstall": "?", + "files": "?", + "languages": "?", + "media": "?", + "updateservers": "!", + "config": "?", + "dlid": "?" + }, + "files": { + "filename": "*", + "folder": "*" + }, + "languages": { + "language": "*" + }, + "media": { + "filename": "*", + "folder": "*" + }, + "install": { + "sql": "*" + }, + "update": { + "sql": "*", + "schemas": "*" + }, + "uninstall": { + "sql": "*" + }, + "sql": { + "file": "*" + }, + "updateservers": { + "server": "*" + }, + "config": { + "fields": "!" + }, + "fields": { + "fieldset": "+" + }, + "fieldset": { + "field": "+" + }, + "field": { + "option": "*" + } + }, + "attributes": { + "extension": [ + "client", + "method", + "overwrite", + "type", + "version" + ], + "files": [ + "folder" + ], + "filename": [ + "module" + ], + "languages": [ + "folder" + ], + "language": [ + "client", + "tag" + ], + "media": [ + "destination", + "folder" + ], + "file": [ + "charset", + "driver" + ], + "server": [ + "name", + "priority", + "type" + ], + "fields": [ + "addfieldpath", + "name" + ], + "fieldset": [ + "name" + ], + "field": [ + "default", + "description", + "label", + "name", + "type" + ], + "option": [ + "value" + ] + } +} \ No newline at end of file diff --git a/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_plugin.json b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_plugin.json new file mode 100644 index 0000000..45bd081 --- /dev/null +++ b/administrator/components/com_jedchecker/libraries/rules/xmlmanifest_plugin.json @@ -0,0 +1,118 @@ +{ + "nodes": { + "extension": { + "name": "!", + "element": "?", + "creationDate": "=", + "author": "=", + "authorEmail": "=", + "authorUrl": "=", + "copyright": "=", + "version": "!", + "description": "=", + "license": "!", + "scriptfile": "?", + "install": "?", + "update": "?", + "uninstall": "?", + "files": "?", + "languages": "?", + "media": "?", + "updateservers": "!", + "config": "?", + "dlid": "?" + }, + "files": { + "filename": "*", + "folder": "*" + }, + "languages": { + "language": "*" + }, + "media": { + "filename": "*", + "folder": "*" + }, + "install": { + "sql": "*" + }, + "update": { + "sql": "*", + "schemas": "*" + }, + "uninstall": { + "sql": "*" + }, + "sql": { + "file": "*" + }, + "updateservers": { + "server": "*" + }, + "config": { + "fields": "!" + }, + "fields": { + "fieldset": "+" + }, + "fieldset": { + "field": "+" + }, + "field": { + "option": "*" + } + }, + "attributes": { + "extension": [ + "client", + "group", + "method", + "overwrite", + "type", + "version" + ], + "files": [ + "folder" + ], + "filename": [ + "plugin" + ], + "languages": [ + "folder" + ], + "language": [ + "client", + "tag" + ], + "media": [ + "destination", + "folder" + ], + "file": [ + "charset", + "driver" + ], + "server": [ + "name", + "priority", + "type" + ], + "fields": [ + "addfieldpath", + "name" + ], + "fieldset": [ + "name" + ], + "field": [ + "default", + "description", + "label", + "name", + "type" + ], + "option": [ + "value" + ] + } +} \ No newline at end of file