Sindbad~EG File Manager
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Definition helper to make classes available via webservices.
*
* @author Guy Thomas
* @copyright Copyright (c) 2016 Open LMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace theme_snap\webservice;
defined('MOODLE_INTERNAL') || die();
use external_single_structure;
use external_multiple_structure;
use external_value;
use coding_exception;
use moodle_url;
use cache;
require_once($CFG->dirroot.'/lib/externallib.php');
/**
* Definition helper class.
* Note: If you are using this class, please be aware that you need to purge caches to force new definitions when
* classes change. To ensure this happens for a release you can simply version bump your plugin.
*
* @author Guy Thomas
* @copyright Copyright (c) 2016 Open LMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class definition_helper {
/**
* @var string
*/
protected $classname;
/**
* @var array
*/
protected $usenamespaces = [];
/**
* @var external_value[];
*/
protected $definition;
/**
* definition_helper constructor.
* @param string|object $classorobject
*/
public function __construct($classorobject) {
$reflect = new \ReflectionClass($classorobject);
$this->classname = $reflect->getName();
$cacheddefinition = $this->get_definition_from_cache($this->classname);
if ($cacheddefinition) {
$this->definition = $cacheddefinition;
return;
}
$this->set_use_namespaces($classorobject);
$this->define_class_for_webservice_internal($classorobject);
$this->cache_definition($this->classname, $this->definition);
}
/**
* Get definition for classname from cache.
* @param string $classname
*/
private function get_definition_from_cache($classname) {
$cache = cache::make('theme_snap', 'webservicedefinitions');
$data = $cache->get($classname);
return unserialize($data);
}
/**
* Cache definition for specific classname.
* @param string $classname
* @param external_value[] $definition
*/
private function cache_definition($classname, $definition) {
$cache = cache::make('theme_snap', 'webservicedefinitions');
$cache->set($classname, serialize($definition));
}
/**
* Get defintion.
* @return external_value[]
*/
public function get_definition() {
return $this->definition;
}
/**
* Set namespaces used by the class or object.
* @param $classorobject
*/
private function set_use_namespaces($classorobject) {
$reflect = new \ReflectionClass($classorobject);
$classfilename = $reflect->getFileName();
if (empty($classfilename)) {
throw new coding_exception('Failed to get filename for class '.$reflect->getName());
}
$contents = file_get_contents($classfilename);
if (!$contents) {
return;
}
$matches = [];
preg_match_all('/(?<=use)\s(\S*);/m', $contents, $matches);
if (count($matches) < 2) {
return;
}
$this->usenamespaces = $matches[1];
}
/**
* Create an external value from an object containing type, description, required properties.
* @param stdClass $obj
* @return external_value
* @throws coding_exception
*/
private function create_external_value_from_obj($obj) {
if (empty($obj->type)) {
throw new coding_exception('Object is missing type property');
}
$type = $obj->type;
$description = !empty($obj->description) ? $obj->description : null;
if (isset($obj->required)) {
$required = !empty($obj->required) || $obj->required === false ? $obj->required : null;
if (!is_bool($required)) {
// If null, required will be false by default.
$required = strtolower($required) === 'true';
}
} else {
$required = false;
}
if (isset($obj->allownull)) {
$allownull = !empty($obj->allownull) || $obj->allownull === false ? $obj->allownull : null;
if (!is_bool($allownull)) {
// If null, allownull will be true by default.
$allownull = !strtolower($allownull) === 'false';
}
} else {
$allownull = true;
}
$extval = new external_value($type, $description, $required, null, $allownull);
return $extval;
}
/**
* Converts an object's 'PARAM_TYPE' string values into constants OR classes into new definitions.
*
* @param $obj
*/
private function convert_object_params($obj) {
foreach ($obj as $key => $val) {
if (isset($val->type) && is_string($val->type)) {
if (defined($val->type)) {
$obj->$key->type = constant($val->type);
$obj->$key = $this->create_external_value_from_obj($obj->$key);
} else if (strpos($val->type, '{') !== false) {
$obj->$key->type = $this->convert_ws_param_to_object($val->type);
} else {
$classdetected = $this->get_class_from_type($val->type);
if ($classdetected) {
if ($classdetected === $this->classname) {
throw new coding_exception($this->param_error('Class definition infinite recursion ', $key));
}
$isarray = strpos($val->type, '[]') !== false;
if ($isarray) {
$defineobj = new definition_helper($classdetected);
if (empty($val->description)) {
$val->description = '';
}
$obj->$key = new external_multiple_structure(
new external_single_structure((array) $defineobj->get_definition(), $val->description)
);
} else {
$obj->$key = new external_single_structure((array) $defineobj, $val->description);
}
} else {
throw new coding_exception(
$this->param_error('Unable to process type '.$val->type, $key)
);
}
}
} else {
if (is_object($obj->$key)) {
$obj->$key = $this->convert_object_params($val);
} else {
throw new coding_exception('Type not specified', var_export($obj, true));
}
}
}
}
/**
* Convert a json style wsparam to an object.
* NOTE: Will work with relaxed json - e.g.
* {
* description: "My description"
* };
* as opposed to
* {
* "description" : "My description"
* };
* @param $comment
* @return [$obj, $isarray]
*/
private function convert_ws_param_to_object($comment) {
$matches = [];
$regex = '/(?<=@wsparam)(?:\s*{)(.*)(};|}\[\];)/s';
$haswsparamdoc = preg_match($regex, $comment, $matches);
if ($haswsparamdoc !== 1) {
return ['object' => false, 'isarray' => false];
}
$isarray = strpos($matches[2], '[]') !== false;
$result = $matches[1];
// Remove comment asterixes.
$result = '{' . "\n" . preg_replace('/(^\s*\*)/m', "", $result) . '}';
// Make sure keys have strings round them.
$regex = '/^\s*([^":{}]*):/m';
$result = preg_replace_callback($regex, function($matches) {
$str = trim($matches[1]);
$str = '"' . $str . '"';
return str_replace($matches[1], $str, $matches[0]);
}, $result);
// Make sure values have strings round them, where appropriate.
$regex = '/(?<=:)([^"{},]*)(?:,|$)/m';
$result = preg_replace_callback($regex, function($matches) {
$str = trim($matches[1]);
// Exclude numbers.
if (strval(floatval($str)) === $str) {
return $matches[0];
}
// Exclude true / false.
$excs = ['true', 'false'];
foreach ($excs as $exc) {
if (strtolower($str) === $exc) {
return $matches[0];
}
}
$str = '"' . $str . '"';
return str_replace($matches[1], $str, $matches[0]);
}, $result);
$obj = json_decode($result);
$this->convert_object_params($obj);
return [$obj, $isarray];
}
/**
* @param string $errmsg - error message.
* @param string $name - property name.
* @return string
*/
private function param_error($errmsg, $name) {
if (substr($errmsg, -1) !== ' ') {
$errmsg .= ' ';
}
return $errmsg . 'for ' . $name . ' in class ' . $this->classname;
}
/**
* Defines a property from a @wsparam
* "@wsparam" is intended for documenting stdClass via json.
* @param $comment
* @return bool
* @throws coding_exception
*/
private function define_from_ws_param($name, $comment) {
$matches = [];
$regex = '/(?<=\*\s@wsparam\s)(\S*)\s(.*)/';
$haswsparamdoc = preg_match($regex, $comment, $matches);
if ($haswsparamdoc !== 1) {
return false;
}
if (strpos($comment, '{') === false) {
throw new coding_exception($this->param_error('@wsparam not defined with json', $name));
}
// The comment was formatted as json.
list ($obj, $isarray) = $this->convert_ws_param_to_object($comment);
if (!$obj) {
throw new coding_exception($this->param_error('Failed to decode @wsparam json', $name));
}
if (isset($obj->type) && is_string($obj->type)) {
// @codingStandardsIgnoreStart
// The comment is defining just this parameter - e.g:.
/**
* @wsparam {
* type: PARAM_INT,
* description: "Counter",
* required: true
* };
*/
// @codingStandardsIgnoreEnd
if (empty($obj->description)) {
throw new coding_exception($this->param_error('Missing description for', $name));
}
$required = !empty($obj->required);
return new external_value($obj->type, $obj->description, $required);
} else {
// @codingStandardsIgnoreStart
// The comment is defining an object - i.e. StdClass - e.g:.
/**
* @wsparam {
* progress: {
* type: PARAM_INT,
* description: "Student progress",
* required: true
* },
* total: {
* type: PARAM_INT,
* description: "Total to complete"
* }
* };
*/
// @codingStandardsIgnoreEnd
if ($isarray) {
return new external_multiple_structure(new external_single_structure((array)$obj));
} else {
return new external_single_structure((array)$obj);
}
}
}
/**
* Get a single line @ws doc entry from comment - @wstype, @wsdesc, @wsrequired, @wsallownull (excludes @wsparam).
* @param string|array $key
* @return string | false;
*/
private function get_wsdoc($comment, $key) {
if (is_array($key)) {
$keys = $key;
} else {
$keys = [$key];
}
foreach ($keys as $key) {
if ($key === '@wsallownull' || $key === '@wsrequired') {
// For allownull / required, we need to check for just an empty doc and accept that as true.
$regex = '/\s*\*\s@wsrequired(\n|$)/';
if (preg_match($regex, $comment) === 1) {
return true;
}
}
$regex = '/(?<=\*\s'.$key.')\s*(.*)(?:\n|$)/';
$hasdoc = preg_match($regex, $comment, $matches);
$val = $hasdoc && count($matches) > 1 ? $matches[1] : false;
if ($val) {
if ($key === '@wsallownull' || $key === '@wsrequired') {
if (strtolower(trim($val)) === 'true' || trim($val) === '1') {
return true;
} else {
return false;
}
}
return $val;
}
}
if ($key === '@wsallownull') {
return true; // Failed to find but return default value of true.
}
return false; // Failed to find.
}
/**
* Get class from type.
* @param string $type
* @return bool|mixed|string
*/
private function get_class_from_type($type) {
$reflect = new \ReflectionClass($this->classname);
$chktype = str_replace('[]', '', $type);
$mainnamespace = $reflect->getNamespaceName();
if (class_exists($chktype)) {
return $chktype;
} else if (class_exists($mainnamespace . '\\' . $chktype)) {
return $mainnamespace . '\\' . $chktype;
} else {
foreach ($this->usenamespaces as $namespace) {
$regex = '/\\\\' . $chktype . '$/';
if (preg_match($regex, $namespace) === 1) {
if (class_exists($namespace)) {
return $namespace;
}
}
}
}
return false;
}
/**
* Returns an array of external_value objects based on a class or object for use with defining a webservice.
*
* @param $classorobject
* @throws coding_exception
*/
private function define_class_for_webservice_internal($classorobject) {
$reflect = new \ReflectionClass($classorobject);
$public = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC);
$this->definition = [];
foreach ($public as $property) {
$name = $property->getName();
$comment = $property->getDocComment();
$classdetected = '';
$isarray = false;
// Do we have a @wsparam to define this property?
$regex = '/(?<=\*\s@wsparam\s)(\S*)\s(.*)/';
$haswsparamdoc = preg_match($regex, $comment);
if ($haswsparamdoc === 1) {
$this->definition[$name] = $this->define_from_ws_param($name, $comment);
continue;
}
// Do we have a @wstype to define the type of this property?
$wstype = $this->get_wsdoc($comment, '@wstype');
if ($wstype) {
$wstype = constant($wstype);
}
// Do we have a @wsdescription / @wsdesc to define the description of this property?
$wsdesc = $this->get_wsdoc($comment, ['@wsdescription', '@wsdesc']);
// Do we have a @wsrequired to define the required status of this property?
$wsreq = $this->get_wsdoc($comment, ['@wsrequired']);
// Do we have a @wsallownull?
$wsallownull = $this->get_wsdoc($comment, ['@wsallownull']);
// Get information from @var doc.
$regex = '/(?<=\*\s@var\s)(\S*)([^\S\x0a\x0d].*|)/m';
$matches = [];
$hasvardoc = preg_match($regex, $comment, $matches);
if ($hasvardoc !== 1 && (empty($wstype) && empty($wsdesc))) {
throw new coding_exception('Property '.$name.' missing @var or @wsparam doc in class '.$this->classname);
}
// Establish description.
if (!empty($wsdesc)) {
// Use @wsdesc.
$description = $wsdesc;
} else {
// Extract from @var.
if (count($matches) < 3) {
$description = '';
} else {
$description = trim($matches[2]);
}
}
// Establish type.
if (!empty($wstype)) {
// Use @wstype.
$type = $wstype;
} else {
// Extract from @var.
$aliases = [
'bool' => PARAM_BOOL,
'boolean' => PARAM_BOOL,
'str' => PARAM_RAW,
'string' => PARAM_RAW,
'int' => PARAM_INT,
'integer' => PARAM_INT,
'float' => PARAM_FLOAT,
'moodle_url' => PARAM_URL,
];
$type = trim($matches[1]);
if (isset($aliases[$type])) {
$type = $aliases[$type];
} else if (isset($aliases[str_replace('\\', '', $type)])) {
$type = $aliases[str_replace('\\', '', $type)];
} else {
if ($type === 'stdClass') {
throw new coding_exception(
'Property ' . $name . ' using unspoported stdClass in class ' . $this->classname
);
}
$isarray = strpos($type, '[]');
$classdetected = $this->get_class_from_type($type);
if (empty($classdetected)) {
throw new coding_exception(
$this->param_error('Unknown / incompatible var type ' . $type, $name)
);
}
if ($classdetected === $this->classname) {
throw new coding_exception($this->param_error('Class definition infinite recursion ', $name));
}
}
}
if ($classdetected) {
$defineobj = new definition_helper($classdetected);
$structure = new external_single_structure($defineobj->get_definition(), $description, $wsreq);
if ($isarray) {
$this->definition[$name] = new external_multiple_structure($structure, $description, $wsreq);
} else {
$this->definition[$name] = $structure;
}
} else {
$this->definition[$name] = new external_value($type, $description, $wsreq, null, $wsallownull);
}
}
}
/**
* Convenience function.
* @param string|object $classorobject
*/
public static function define_class_for_webservice($classorobject) {
$helper = new definition_helper($classorobject);
return $helper->get_definition();
}
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists