diff --git a/openthess/modules/services/LICENSE.txt b/openthess/modules/services/LICENSE.txt new file mode 100755 index 0000000..d159169 --- /dev/null +++ b/openthess/modules/services/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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 2 of the License, or + (at your option) any later version. + + This program 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 this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/openthess/modules/services/README.txt b/openthess/modules/services/README.txt new file mode 100644 index 0000000..158b9a8 --- /dev/null +++ b/openthess/modules/services/README.txt @@ -0,0 +1,33 @@ + +Goals +============== +- Create a unified Drupal API for web services to be exposed in a variety of + different server formats. +- Provide a service browser to be able to test methods. +- Allow distribution of API keys for developer access. + +Documentation +============== +http://drupal.org/node/109782 + +Installation +============ +If you are using the rest server you will need to download the latest version of SPYC: +wget https://raw.github.com/mustangostang/spyc/79f61969f63ee77e0d9460bc254a27a671b445f3/spyc.php -O servers/rest_server/lib/spyc.php + +Once downloaded you need to add spyc.php to the rest_server/lib folder which exists under +the location you have installed services in. + +Documentation files +=================== +You can find these files in /docs folder. +services.authentication.api.php -- hooks related to authentication plugins +services.servers.api.php -- servers definition hooks +services.services.api.php -- definition of new services +services.versions.api.php -- how to write versioned resources + +Settings via variables +====================== + +'services_{$resource}_index_page_size' -- this variable controls maximum number of results that +will be displayed by index query. See services_resource_build_index_query() for more information. diff --git a/openthess/modules/services/auth/services_oauth/services_oauth.css b/openthess/modules/services/auth/services_oauth/services_oauth.css new file mode 100644 index 0000000..aaed13c --- /dev/null +++ b/openthess/modules/services/auth/services_oauth/services_oauth.css @@ -0,0 +1,4 @@ +.auth-authorization > .fieldset-wrapper > .form-item { + float: left; + margin-right: 20px; +} \ No newline at end of file diff --git a/openthess/modules/services/auth/services_oauth/services_oauth.inc b/openthess/modules/services/auth/services_oauth/services_oauth.inc new file mode 100644 index 0000000..ad0434b --- /dev/null +++ b/openthess/modules/services/auth/services_oauth/services_oauth.inc @@ -0,0 +1,206 @@ +context !== $settings['oauth_context']) { + throw new OAuthException('The consumer is not valid in the current context'); + } + + // Validate the token, if it's required by the method + if ($cred == 'token') { + if (empty($token->key)) { + throw new OAuthException('Missing access token'); + } + if (!$token->authorized) { + throw new OAuthException('The access token is not authorized'); + } + // Check that the consumer has been granted the required authorization level + if (!empty($auth_level) && !in_array('*', $token->services) && !in_array($auth_level, $token->services)) { + throw new OAuthException('The consumer is not authorized to access this service'); + } + } + + // Add the oauth authentication info to server info + services_set_server_info('oauth_consumer', $consumer); + services_set_server_info('oauth_token', $token); + + // Load the user if the request was authenticated using a token + // that's associated with a account. + if ($cred == 'token') { + if ($token->uid) { + global $user; + $user = user_load($token->uid); + } + } + else if ($cred == 'consumer') { + if ($consumer->uid) { + // This authenticates as the user who owns 'key'; It is for 2-stage + // OAuth and is vastly inferior to 3-stage OAuth. + global $user; + $user = user_load($consumer->uid); + } + } + } + catch (OAuthException $e) { + drupal_add_http_header('WWW-Authenticate', sprintf('OAuth realm="%s"', url('', array('absolute' => TRUE)))); + return $e->getMessage(); + } +} + +function _services_oauth_security_settings_authorization($settings) { + return isset($settings['authorization']) ? $settings['authorization'] : ''; +} + +function _services_oauth_security_settings($settings, &$form_state) { + if (isset($form_state['values']['services_oauth']['oauth_context'])) { + $settings['oauth_context'] = $form_state['values']['services_oauth']['oauth_context']; + } + + $form = array(); + $form['oauth_context'] = array( + '#type' => 'select', + '#options' => array('' => t('-- Select an OAuth context')), + '#default_value' => isset($settings['oauth_context']) ? $settings['oauth_context'] : '', + '#title' => t('OAuth context'), + '#description' => t('The OAuth contexts provides a scope for consumers and authorizations and have their own authorization levels. Different services endpoints may share OAuth contexts and thereby allow the use of consumers and tokens across the services endpoint boundraries.'), + ); + $form['authorization'] = array( + '#type' => 'select', + '#options' => array(), + '#default_value' => isset($settings['authorization']) ? $settings['authorization'] : '', + '#title' => t('Default required OAuth Authorization level'), + '#description' => t('The default OAuth authorization level that will be required to access resources.'), + ); + + $contexts = oauth_common_context_load_all(); + foreach ($contexts as $context) { + $form['oauth_context']['#options'][$context->name] = $context->title; + if (isset($context->authorization_levels) && $context->name == $settings['oauth_context']) { + foreach ($context->authorization_levels as $name => $level) { + $form['authorization']['#options'][$name] = t($level['title']) . " ({$context->name})"; + } + } + } + if (empty($form['authorization']['#options'])) { + $form['authorization'] = array( + '#type' => 'item', + '#title' => t('Select an OAuth context enable default required OAuth Authorization level') + ) + $form['authorization']; + } + else { + $form['authorization']['#options'] = array('' => t('None')) + $form['authorization']['#options']; + } + + $form['credentials'] = array( + '#type' => 'select', + '#options' => array( + 'none' => t('None, OAuth authentication will be disabled by default'), + 'unsigned_consumer' => t('Unsigned with consumer key'), + 'consumer' => t('Consumer key, also known as 2-legged OAuth'), + 'token' => t('Consumer key and access token, also known as 3-legged OAuth'), + ), + '#default_value' => $settings['credentials'], + '#title' => t('Default required authentication'), + '#description' => t('Authorization levels will not be applied if the consumer isn\'t required to supply a access token.'), + ); + + return $form; +} + +function _services_oauth_default_security_settings() { + return array( + 'oauth_context' => '', + 'authorization' => '', + 'credentials' => 'token', + ); +} + +function _services_oauth_controller_settings($settings, $controller, $endpoint, $class, $name) { + $form = array(); + $cc = array( + 'credentials' => '', + 'authorization' => '', + ); + if (!empty($controller['endpoint']['services_oauth'])) { + $cc = $controller['endpoint']['services_oauth'] + $cc; + } + $auth_levels = array(); + if (is_array($endpoint) && isset($endpoint['oauth_context'])) { + $context = oauth_common_context_load($endpoint['oauth_context']); + if (isset($context->authorization_levels)) { + foreach ($context->authorization_levels as $name => $level) { + $auth_levels[$name] = t($level['title']); + } + } + } + + $form['credentials'] = array( + '#type' => 'select', + '#options' => array( + '' => t('Default'), + 'none' => t('None'), + 'unsigned_consumer' => t('Unsigned with consumer key'), + 'consumer' => t('Consumer key'), + 'token' => t('Consumer key and access token'), + ), + '#default_value' => isset($settings['credentials']) ? $settings['credentials'] : '', + '#title' => t('Required authentication'), + '#description' => t('Authorization levels will not be applied if the consumer isn\'t required to supply a access token.'), + ); + + $form['authorization'] = array( + '#type' => 'select', + '#options' => array( + '' => t('Default'), + ) + $auth_levels, + '#default_value' => isset($settings['authorization']) ? $settings['authorization'] : '', + '#title' => t('Required authorization'), + ); + + return $form; +} diff --git a/openthess/modules/services/auth/services_oauth/services_oauth.info b/openthess/modules/services/auth/services_oauth/services_oauth.info new file mode 100644 index 0000000..a79aa78 --- /dev/null +++ b/openthess/modules/services/auth/services_oauth/services_oauth.info @@ -0,0 +1,13 @@ +name = OAuth Authentication +description = Provides OAuth authentication for the services module +package = Services - authentication +dependencies[] = services +dependencies[] = oauth_common +core = 7.x +php = 5.2 +; Information added by Drupal.org packaging script on 2014-01-31 +version = "7.x-3.7" +core = "7.x" +project = "services" +datestamp = "1391207946" + diff --git a/openthess/modules/services/auth/services_oauth/services_oauth.install b/openthess/modules/services/auth/services_oauth/services_oauth.install new file mode 100644 index 0000000..fa249cd --- /dev/null +++ b/openthess/modules/services/auth/services_oauth/services_oauth.install @@ -0,0 +1,45 @@ + 'services_oauth.inc', + 'title' => t('OAuth authentication'), + 'description' => t('An open protocol to allow secure API authorization'), + 'security_settings' => '_services_oauth_security_settings', + 'default_security_settings' => '_services_oauth_default_security_settings', + 'authenticate_call' => '_services_oauth_authenticate_call', + 'controller_settings' => '_services_oauth_controller_settings', + ); +} diff --git a/openthess/modules/services/css/services.admin.css b/openthess/modules/services/css/services.admin.css new file mode 100644 index 0000000..6ff4fce --- /dev/null +++ b/openthess/modules/services/css/services.admin.css @@ -0,0 +1,73 @@ +/* Resource Table */ +#resource-form-table th.select-all { + width: 1em; +} +th.resource_test { + width: 16em; +} + +.resource-image { + display: inline-block; + cursor: pointer; + width: 1em; +} +.resource-group-label label { + display: inline; + font-weight: bold; +} +.resource-test-label label { + margin-left: 1em; /* LTR */ +} +.resource-test-description .description { + margin: 0; +} +#resource-form-table tr td { + background-color: white; + color: #494949; + vertical-align: top; +} +#resource-form-table tr.resource-group td { + background-color: #EDF5FA; + color: #494949; +} + +#resource-form-table tr.resource-method { + border-bottom: 1px solid #494949; +} + +#resource-form-table tr.resource-operation-class { + border-bottom: 2px solid #494949; + border-top: 2px solid #494949; +} + + +table#resource-form-table tr.resource-group label { + display: inline; +} + +div.message > div.item-list { + font-weight: normal; +} +a.resource-collapse { + height: 0; + width: 0; + top: -99em; + position: absolute; +} +a.resource-collapse:focus, +a.resource-collapse:hover { + font-size: 80%; + top: 0px; + height: auto; + width: auto; + overflow: visible; + position: relative; + z-index: 1000; +} +td.resource-group-alias { + width:130px; +} +/* Zebra colors for resources admin table */ +#resource-form-table tr.resource-even td { + background: none repeat scroll 0 0 #F3F4EE; +} \ No newline at end of file diff --git a/openthess/modules/services/docs/services.alter.api.php b/openthess/modules/services/docs/services.alter.api.php new file mode 100644 index 0000000..be10b0b --- /dev/null +++ b/openthess/modules/services/docs/services.alter.api.php @@ -0,0 +1,200 @@ +handle() + */ +function hook_services_endpoint_response_alter(&$response) { + +} + +/** + * Allows alteration of the parsed request data before calling the controller. + * + * @param array $data + * The parsed request data. + * @param array $controller + * The current controller definition. + * + * @see RESTServer->getControllerArguments() + */ +function hook_rest_server_request_parsed_alter(&$data, $controller) { + +} + +/** + * Allows alteration of the parsed request headers before calling the controller. + * + * @param array $data + * The parsed request data. + * + * @see RESTServer->getControllerArguments() + */ +function hook_rest_server_headers_parsed_alter(&$headers) { + +} + +/** + * Allows alteration of error data before the status code or message are returned. + * + * @param array $error_alter_array + * An associative array with the following keys: + * - 'code': the HTTP status code. + * - 'header_message': the message returned as part of the error response + * (for instance, "404 Not found"). + * - 'body_data': data that was passed to the thrown exception. + * @param array $controller + * The current controller definition. + * @param array $arguments + * Arguments passed to the current controller. + * + * @see RESTServer->handleException() + */ +function hook_rest_server_execute_errors_alter(&$error_alter_array, $controller, $arguments) { + +} + +/** + * Allows alteration of the user object after services removes sensitive information. + * + * @param object $user + * A user object without the 'pass' attribute, and if the current user doesn't + * have the 'administer users' permission, this will also not include the + * 'mail' or 'init' attributes. + * + * @see services_remove_user_data() + */ +function hook_services_account_object_alter(&$user) { + +} diff --git a/openthess/modules/services/docs/services.authentication.api.php b/openthess/modules/services/docs/services.authentication.api.php new file mode 100644 index 0000000..7bee36d --- /dev/null +++ b/openthess/modules/services/docs/services.authentication.api.php @@ -0,0 +1,42 @@ + + * - security_settings: A callback function which returns an associative + * array of Form API elements for a settings form. + * - default_security_settings: A callback funtion which returns an array + * with the default settings for the auth module. + * - _services_security_settings_validate: The name of a standard form + * validation callback for the form defined in 'security_settings'. + * - _services_security_settings_submit: The name of a standard form + * submit callback for the form defined in 'security_settings'. + * - alter_controllers: The name of a callback function which will alter a + * services method signature in order to add required arguments. + * - controller_settings: A callback function which returns an associative + * array of Form API elements for a controller settings form. + * - file: An include file which contains the authentication callbacks. + */ +function hook_services_authentication_info() { + +} diff --git a/openthess/modules/services/docs/services.servers.api.php b/openthess/modules/services/docs/services.servers.api.php new file mode 100644 index 0000000..f5978ef --- /dev/null +++ b/openthess/modules/services/docs/services.servers.api.php @@ -0,0 +1,54 @@ + 'REST', + 'path' => 'rest', + 'settings' => array( + 'file' => array('inc', 'rest_server'), + 'form' => '_rest_server_settings', + 'submit' => '_rest_server_settings_submit', + ), + ); +} + +/** + * Acts on requests to the server defined in hook_server_info(). + * + * This is the main entry point to your server implementation. + * Need to get some more description about the best way to implement + * servers. + */ +function hook_server() { + $endpoint_path = services_get_server_info('endpoint_path', 'services/rest'); + $canonical_path = trim(drupal_substr($_GET['q'], drupal_strlen($endpoint_path)), '/'); + $canonical_path = explode('/', $_GET['q']); + $endpoint_path_count = count(explode('/', $endpoint_path)); + for ($x = 0; $x < $endpoint_path_count; $x++) { + array_shift($canonical_path); + } + $canonical_path = implode('/', $canonical_path); + if (empty($canonical_path)) { + return ''; + } + //Handle server based on $canonical_path +} \ No newline at end of file diff --git a/openthess/modules/services/docs/services.services.api.php b/openthess/modules/services/docs/services.services.api.php new file mode 100644 index 0000000..655f2a3 --- /dev/null +++ b/openthess/modules/services/docs/services.services.api.php @@ -0,0 +1,336 @@ + array('data' => 'nid'), will pass + * them off as a single variable, 'source' => array('data') will give you + * all argument values to each argument. + * - default value: this is a value that will be passed to the method for + * this particular argument if no argument value is passed + * + * A detailed example of creating a new resource can be found at + * http://drupal.org/node/783460 and more information about how + * REST resources are managed can be found at http://drupal.org/node/783254. + */ +function hook_services_resources() { +$node_resource = array( + 'node' => array( + 'operations' => array( + 'retrieve' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_retrieve', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to get', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + ), + 'create' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_create', + 'args' => array( + array( + 'name' => 'node', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The node data to create', + 'type' => 'array', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('create'), + 'access arguments append' => TRUE, + ), + 'update' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_update', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to get', + ), + array( + 'name' => 'node', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The node data to update', + 'type' => 'array', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('update'), + 'access arguments append' => TRUE, + ), + 'delete' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_delete', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('delete'), + 'access arguments append' => TRUE, + ), + 'index' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_index', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters array', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_node_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access arguments' => array('access content'), + ), + ), + 'targeted_actions' => array( + 'attach_file' => array( + 'help' => 'Upload and attach file(s) to a node. POST multipart/form-data to node/123/attach_file', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_attach_file', + 'access callback' => '_node_resource_access', + 'access arguments' => array('update'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to attach a file to', + ), + array( + 'name' => 'field_name', + 'optional' => FALSE, + 'source' => array('data' => 'field_name'), + 'description' => 'The file parameters', + 'type' => 'string', + ), + array( + 'name' => 'attach', + 'optional' => TRUE, + 'source' => array('data' => 'attach'), + 'description' => 'Attach the file(s) to the node. If FALSE, this clears ALL files attached, and attaches the files', + 'type' => 'int', + 'default value' => TRUE, + ), + array( + 'name' => 'field_values', + 'optional' => TRUE, + 'source' => array('data' => 'field_values'), + 'description' => 'The extra field values', + 'type' => 'array', + 'default value' => array(), + ), + ), + ), + ), + 'relationships' => array( + 'files' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'help' => t('This method returns files associated with a node.'), + 'access callback' => '_node_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'callback' => '_node_resource_load_node_files', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node whose files we are getting', + ), + array( + 'name' => 'file_contents', + 'type' => 'int', + 'description' => t('To return file contents or not.'), + 'source' => array('path' => 2), + 'optional' => TRUE, + 'default value' => TRUE, + ), + array( + 'name' => 'image_styles', + 'type' => 'int', + 'description' => t('To return image styles or not.'), + 'source' => array('path' => 3), + 'optional' => TRUE, + 'default value' => FALSE, + ), + ), + ), + ), + ), + ); + if (module_exists('comment')) { + $comments = array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'help' => t('This method returns the number of new comments on a given node.'), + 'access callback' => 'user_access', + 'access arguments' => array('access comments'), + 'access arguments append' => FALSE, + 'callback' => '_node_resource_load_node_comments', + 'args' => array( + array( + 'name' => 'nid', + 'type' => 'int', + 'description' => t('The node id to load comments for.'), + 'source' => array('path' => 0), + 'optional' => FALSE, + ), + array( + 'name' => 'count', + 'type' => 'int', + 'description' => t('Number of comments to load.'), + 'source' => array('param' => 'count'), + 'optional' => TRUE, + ), + array( + 'name' => 'offset', + 'type' => 'int', + 'description' => t('If count is set to non-zero value, you can pass also non-zero value for start. For example to get comments from 5 to 15, pass count=10 and start=5.'), + 'source' => array('param' => 'offset'), + 'optional' => TRUE, + ), + ), + ); + $node_resource['node']['relationships']['comments'] = $comments; + } + return $node_resource; +} \ No newline at end of file diff --git a/openthess/modules/services/docs/services.versions.api.php b/openthess/modules/services/docs/services.versions.api.php new file mode 100644 index 0000000..7e499a2 --- /dev/null +++ b/openthess/modules/services/docs/services.versions.api.php @@ -0,0 +1,52 @@ + 'Create a node with an nid test', + ); + return $new_set; +} + +function _system_resource_set_variable_update_1_2() { + $new_set = array( + 'help' => 'Create a node with an nid optional prams.', + 'args' => array( + array( + 'name' => 'name', + 'optional' => TRUE, + 'source' => array('data' => 'name'), + 'description' => t('The name of the variable to set.'), + 'type' => 'string', + ), + array( + 'name' => 'value', + 'optional' => TRUE, + 'source' => array('data' => 'value'), + 'description' => t('The value to set.'), + 'type' => 'string', + ), + ), + ); + return $new_set; +} diff --git a/openthess/modules/services/includes/services.resource_build.inc b/openthess/modules/services/includes/services.resource_build.inc new file mode 100644 index 0000000..8723f33 --- /dev/null +++ b/openthess/modules/services/includes/services.resource_build.inc @@ -0,0 +1,210 @@ + &$resource) { + foreach ($class_info as $class_name => $class) { + if (!isset($resource[$class_name])) { + continue; + } + + foreach (array_keys($resource[$class_name]) as $action_name) { + $controllers["{$resource_name}/{$class['class_singular']}/{$action_name}"] = &$resource[$class_name][$action_name]; + } + } + } + + // Make sure that we got a access callback for all resources + foreach ($controllers as &$controller) { + if (!empty($controller['file'])) { + // Convert old-style file references to something that fits module_load_include() better. + if (!empty($controller['file']['file']) && empty($controller['file']['type'])) { + $controller['file']['type'] = $controller['file']['file']; + } + } + if (!isset($controller['access callback'])) { + $controller['access callback'] = 'user_access'; + } + } + // This hook is deprecated and will be removed in next versions of services. + // Use hook_services_resources_alter instead. + drupal_alter('services_resources_post_processing', $resources, $endpoint); + + // Do some endpoint-dependent processing. + if ($endpoint) { + // Let the authentication modules alter our controllers + foreach ($endpoint->authentication as $auth_module => $auth_settings) { + services_auth_invoke($auth_module, 'alter_controllers', $auth_settings, $controllers, $endpoint); + } + + // Apply any aliases from endpoint + $aliased = array(); + foreach ($resources as $key => $def) { + $def['key'] = $key; + if (!empty($def['endpoint']['alias'])) { + $aliased[$def['endpoint']['alias']] = $def; + } + else { + $aliased[$key] = $def; + } + } + $resources = $aliased; + } + + return $resources; +} + +/** + * Upgrades old resource definition to the newer format. + * + * @param int $from + * The API version that the resource was written for. + * @param array $resources + * The resource definitions. + * @param array $version_info + * Optional. The version info array as returned from services_resource_api_version_info(). + * @return void + */ +function _services_resource_upgrade($from, &$resources, $version_info = NULL) { + module_load_include('inc', 'services', 'includes/services.resource_update'); + + // Get version info if needed. + if ($version_info == NULL) { + $version_info = services_resource_api_version_info(); + } + + // Run upgrades. + foreach ($version_info['versions'] as $update) { + if ($update > $from) { + call_user_func_array("services_resource_api_update_{$update}", array(&$resources)); + } + } +} + +/** + * Applies the endpoint to a set of resources. Resources and controllers that + * aren't supported will be removed (if $strict is set to TRUE) and both + * resources and controllers will get the 'endpoint' attribute set. + * + * @param array $resources + * An array of resources that the endpoint should be applied on. + * @param array $endpoint + * A endpoint information array. + * @param bool $strict + * Optional. + * @return void + */ +function _services_apply_endpoint(&$resources, $endpoint, $strict = TRUE) { + if (is_array($endpoint) && isset($endpoint['build_info'])) { + $endpoint = $endpoint['build_info']['args'][0]; + } + $classes = array_keys(services_operation_class_info()); + foreach ($resources as $name => &$resource) { + $cres = ($endpoint && isset($endpoint->resources[$name])) ? $endpoint->resources[$name] : array(); + if (isset($resource['key']) && $resource['key'] !== $name && $endpoint && isset($endpoint->resources[$resource['key']])) { + $cres = $endpoint->resources[$resource['key']]; + } + $resource['endpoint'] = $cres; + if ($strict && empty($cres)) { + unset($resources[$name]); + } + else { + foreach ($classes as $class) { + if (!empty($resource[$class])) { + foreach ($resource[$class] as $op => $def) { + $cop = isset($cres[$class][$op]) ? $cres[$class][$op] : array(); + if (empty($cop) || !$cop['enabled']) { + if ($strict) { + unset($resource[$class][$op]); + } + } + else { + $resource[$class][$op]['endpoint'] = empty($cop['settings']) ? array() : $cop['settings']; + if (isset($cres['alias'])) { + $resource[$class][$op]['endpoint']['alias'] = $cres['alias']; + } + } + } + } + } + } + } +} + +/** + * Supplies the resource definitions for Drupal core data + * + * @return array + */ +function _services_core_resources() { + module_load_include('inc', 'services', 'resources/comment_resource'); + module_load_include('inc', 'services', 'resources/file_resource'); + module_load_include('inc', 'services', 'resources/node_resource'); + module_load_include('inc', 'services', 'resources/system_resource'); + module_load_include('inc', 'services', 'resources/taxonomy_resource'); + module_load_include('inc', 'services', 'resources/user_resource'); + + $resources = array( + '#api_version' => 3002, + ); + + $resources += _comment_resource_definition(); + $resources += _file_resource_definition(); + $resources += _node_resource_definition(); + $resources += _system_resource_definition(); + $resources += _taxonomy_resource_definition(); + $resources += _user_resource_definition(); + + return $resources; +} diff --git a/openthess/modules/services/includes/services.resource_update.inc b/openthess/modules/services/includes/services.resource_update.inc new file mode 100644 index 0000000..e9f416e --- /dev/null +++ b/openthess/modules/services/includes/services.resource_update.inc @@ -0,0 +1,21 @@ + &$resource) { + foreach ($operations as $key) { + if (!empty($resource[$key])) { + $resource['operations'][$key] = $resource[$key]; + unset($resource[$key]); + } + } + } +} diff --git a/openthess/modules/services/includes/services.runtime.inc b/openthess/modules/services/includes/services.runtime.inc new file mode 100644 index 0000000..4b79d9e --- /dev/null +++ b/openthess/modules/services/includes/services.runtime.inc @@ -0,0 +1,458 @@ +data = !empty($data) ? $data : $message; + } + + /** + * Returns the data associated with the exception. + * + * @return mixed + */ + public function getData() { + return $this->data; + } +} + +/** + * A exception thrown by services and related modules when an error related to + * a specific argument is encountered. + */ +class ServicesArgumentException extends ServicesException { + private $argument; + + /** + * Constructor for the ServicesException. + * + * @param string $message + * Error message. + * @param string $argument_name + * The name of the argument that caused the error. + * @param int $code + * Optional. Error code. This often maps to the HTTP status codes. Defaults + * to 0. + * @param mixed $data + * Information that can be used by the server to return information about + * the error. + */ + public function __construct($message, $argument_name, $code, $data) { + parent::__construct($message, $code, $data); + + $this->argument = $argument_name; + } + + /** + * Returns the name of the argument that caused the error. + * + * @return string + * The name of the argument. + */ + public function getArgumentName() { + return $this->argument; + } +} + +/** + * Performs access checks and executes a services controller. + * This method is called by server implementations. + * + * @param array $controller + * An array containing information about the controller + * @param array $args + * The arguments that should be passed to the controller. + * @param array $options + * Options for the execution. Use 'skip_authentication'=>TRUE to skip the + * services-specific authentication checks. Access checks will always be + * made. + */ +function services_controller_execute($controller, $args = array(), $options = array()) { + $server_info = services_server_info_object(); + if ($server_info->debug) { + watchdog('services', 'Controller:
@controller
', array('@controller' => print_r($controller, TRUE)), WATCHDOG_DEBUG); + watchdog('services', 'Passed arguments:
@arguments
', array('@arguments' => print_r($args, TRUE)), WATCHDOG_DEBUG); + } + + // Switch to anonymous user preserving currently session authenticated user. + _services_controller_execute_preserve_user_switch_anonymous($controller); + + try { + $result = _services_controller_execute_internals($controller, $args, $options); + } + catch (Exception $exception) { + _services_controller_execute_restore_user(); + throw $exception; + } + + _services_controller_execute_restore_user(); + + if ($server_info->debug) { + watchdog('services', 'results:
@results
', array('@results' => print_r($result, TRUE)), WATCHDOG_DEBUG); + } + return $result; +} + +/** + * As authentication methods should authenticate user themselves changing global $user variable + * we preserve incoming session authenticated user and his session so changes made by authentication + * do not interfere. + */ +function _services_controller_execute_preserve_user_switch_anonymous($controller) { + global $user; + services_set_server_info('original_user', $user); + + $preserve_session = strpos($controller['callback'], 'login') === FALSE && strpos($controller['callback'], 'logout') === FALSE; + services_set_server_info('preserve_session', $preserve_session); + if ($preserve_session) { + $original_session_state = drupal_save_session(); + services_set_server_info('original_session_state', $original_session_state); + drupal_save_session(FALSE); + } + + $user = drupal_anonymous_user(); + $user->timestamp = time(); +} + +function _services_controller_execute_restore_user() { + if (services_get_server_info('preserve_session', FALSE)) { + $original_user = services_get_server_info('original_user'); + $original_session_state = services_get_server_info('original_session_state'); + global $user; + $user = $original_user; + drupal_save_session($original_session_state); + } +} + +/** + * Internals of the services_controller_execute(). + * + * Arguments are the same as services_controller_execute(). + */ +function _services_controller_execute_internals($controller, $args, $options) { + $server_info = services_server_info_object(); + $endpoint_name = services_get_server_info('endpoint'); + $endpoint = services_endpoint_load($endpoint_name); + + _services_authenticate_user($controller, $endpoint, $args, $options); + + // Load the proper file. + if (!empty($controller['file']) && $file = $controller['file']) { + module_load_include($file['type'], $file['module'], (isset($file['name']) ? $file['name'] : NULL)); + } + + _services_run_access_callback($controller, $args); + + $endpoint_path = services_get_server_info('endpoint_path', ''); + $endpoint_path_len = drupal_strlen($endpoint_path) + 1; + $canonical_path = drupal_substr($_GET['q'], $endpoint_path_len, drupal_strlen($_GET['q']) - $endpoint_path_len); + + // Prepare $path array and $resource_name. + $path = explode('/', $canonical_path); + $resource_name = array_shift($path); + $options['version'] = _services_version_header_options() ? _services_version_header_options() : NULL; + $options['resource'] = $resource_name; + if (isset($path[0])) { + $options['method'] = $path[0]; + } + + if (!empty($options['version'])) { + services_request_apply_version($controller, $options); + } + + drupal_alter('services_request_preprocess', $controller, $args, $options); + + // Log the arguments. + if ($server_info->debug) { + watchdog('services', 'Called arguments:
@arguments
', array('@arguments' => print_r($args, TRUE)), WATCHDOG_DEBUG); + } + + // Execute the controller callback. + $result = call_user_func_array($controller['callback'], $args); + + drupal_alter('services_request_postprocess', $controller, $args, $result); + + return $result; +} + +/** + * Gets information about a authentication module. + * + * @param string $module + * The module to get info for. + * @return mixed + * The information array, or FALSE if the information wasn't found. + */ +function services_authentication_info($module) { + $info = FALSE; + if (!empty($module) && module_exists($module) && is_callable($module . '_services_authentication_info')) { + $info = call_user_func($module . '_services_authentication_info'); + } + return $info; +} + +/** + * Invokes a authentication module callback. + * + * @param string $module + * The authentication module to invoke the callback for. + * @param string $method + * The callback to invoke. + * @param string $arg1 + * Optional. First argument to pass to the callback. + * @param string $arg2 + * Optional. Second argument to pass to the callback. + * @param string $arg3 + * Optional. Third argument to pass to the callback. + * @return mixed + * + * Aren't these really the following? + * arg1 = Settings + * arg2 = Method + * arg3 = Controller + * + */ +function services_auth_invoke($module, $method, &$arg1 = NULL, &$arg2 = NULL, &$arg3 = NULL) { + // Get information about the auth module + $info = services_authentication_info($module); + drupal_alter('services_authentication_info', $info, $module); + $func = $info && !empty($info[$method]) ? $info[$method] : FALSE; + if (!$func) { + return TRUE; + } + + if (!empty($info['file'])) { + require_once(drupal_get_path('module', $module) . '/' . $info['file']); + } + + if (is_callable($func)) { + $args = func_get_args(); + // Replace module and method name and arg1 with reference to $arg1 and $arg2. + array_splice($args, 0, 5, array(&$arg1, &$arg2, &$arg3)); + return call_user_func_array($func, $args); + } +} + +/** + * Formats a resource uri using the formatter registered through + * services_set_server_info(). + * + * @param array $path + * An array of strings containing the component parts of the path to the resource. + * @return string + * Returns the formatted resource uri, or NULL if no formatter has been registered. + */ +function services_resource_uri($path) { + $endpoint_name = services_get_server_info('endpoint'); + $endpoint = services_endpoint_load($endpoint_name); + if (!empty($path[0]) && !empty($endpoint->resources[$path[0]]['alias'])) { + $path[0] = $endpoint->resources[$path[0]]['alias']; + } + $formatter = services_get_server_info('resource_uri_formatter'); + if ($formatter) { + return call_user_func($formatter, $path); + } + return NULL; +} + +/** + * Sets a server info value + * + * @param string $key + * The key of the server info value. + * @param mixed $value + * The value. + * @return void + */ +function services_set_server_info($key, $value) { + $info = services_server_info_object(); + $info->$key = $value; +} + +/** + * Sets multiple server info values from a associative array. + * + * @param array $values + * An associative array containing server info values. + * @return void + */ +function services_set_server_info_from_array($values) { + $info = services_server_info_object(); + foreach ($values as $key => $value) { + $info->$key = $value; + } +} + +/** + * Gets a server info value. + * + * @param string $key + * The key for the server info value. + * @param mixed $default + * The default value to return if the value isn't defined. + * @return mixed + * The server info value. + */ +function services_get_server_info($key, $default = NULL) { + $info = services_server_info_object(); + $value = $default; + if (isset($info->$key)) { + $value = $info->$key; + } + return $value; +} + +/** + * Gets the server info object. + * + * @param bool $reset + * Pass TRUE if the server info object should be reset. + * @return object + * Returns the server info object. + */ +function services_server_info_object($reset = FALSE) { + static $drupal_static_fast; + if (!isset($drupal_static_fast) || $reset) { + $drupal_static_fast['info'] = &drupal_static(__FUNCTION__, new stdClass()); + } + return $drupal_static_fast['info']; +} + +/** + * Prepare an error message for returning to the server. + * + * @param string $message + * Error message. + * @param int $code + * Optional. Error code. This often maps to the HTTP status codes. Defaults + * to 0. + * @param mixed $data + * Optional. Information that can be used by the server to return information about the error. Defaults to null. + * @return mixed + */ +function services_error($message, $code = 0, $data = NULL) { + throw new ServicesException($message, $code, $data); +} + +/** + * Extract arguments for a services method callback, preserving backwards compatibility with #1083242. + * + * @param array $data + * original argument passed to a resource method callback + * @param string $field + * name of the field where arguments should be checked for + * @return array + */ + +// Adds backwards compatability with regression fixed in #1083242 +function _services_arg_value($data, $field) { + if (isset($data[$field]) && count($data) == 1 && is_array($data[$field])) { + return $data[$field]; + } + return $data; +} + + +/** + * Extract arguments for a services method access callback, preserving backwards compatibility with #1083242. + * + * @param string $data + * original argument passed to a resource method callback + * @param mixed $fields + * name of the field(s) where arguments should be checked for, either as a string or as an array of strings + * @return array + */ + +// Adds backwards compatability with regression fixed in #1083242 +function _services_access_value($data, $fields) { + + if (!is_array($fields)) { + $fields = array($fields); + } + + foreach ($fields as $field) { + if (is_array($data) && isset($data[$field]) && count($data) == 1) { + return $data[$field]; + } + } + return $data; +} + +/** + * Run enabled authentication plugins. + * + * Plugin that authenticate user will change global $user. + */ +function _services_authenticate_user($controller, $endpoint, $args, $options) { + if (!isset($options['skip_authentication']) || !$options['skip_authentication']) { + foreach ($endpoint->authentication as $auth_module => $auth_settings) { + if (!empty($auth_settings)) { + // Add in the auth module's endpoint settings if present. + if (isset($controller['endpoint'][$auth_module])) { + if(is_array($auth_settings)) { + $auth_settings += $controller['endpoint'][$auth_module]; + } + } + if ($auth_error = services_auth_invoke($auth_module, 'authenticate_call', $auth_settings, $controller, $args)) { + return services_error($auth_error, 401); + } + } + } + } +} + +/** + * Call access callback of the method. + */ +function _services_run_access_callback($controller, $args) { + // Construct access arguments array. + if (isset($controller['access arguments'])) { + $access_arguments = $controller['access arguments']; + if (isset($controller['access arguments append']) && $controller['access arguments append']) { + $access_arguments[] = $args; + } + } + else { + // Just use the arguments array if no access arguments have been specified + $access_arguments = $args; + } + + // Load the proper file for the access callback. + if (!empty($controller['access callback file']) && $access_cb_file = $controller['access callback file']) { + $access_cb_file_name = isset($access_cb_file['name']) ? $access_cb_file['name'] : NULL; + module_load_include($access_cb_file['type'], $access_cb_file['module'], $access_cb_file_name); + } + + // Call default or custom access callback. + if (call_user_func_array($controller['access callback'], $access_arguments) != TRUE) { + global $user; + return services_error(t('Access denied for user @user', array( + '@user' => isset($user->name) ? $user->name : 'anonymous', + )), 403); + } +} diff --git a/openthess/modules/services/js/services.admin.js b/openthess/modules/services/js/services.admin.js new file mode 100644 index 0000000..a52adfa --- /dev/null +++ b/openthess/modules/services/js/services.admin.js @@ -0,0 +1,73 @@ +(function ($) { + /** + * Add the table collapsing on the methoding overview page. + */ + Drupal.behaviors.resourceMenuCollapse = { + attach: function (context, settings) { + var timeout = null, + arrowImageHTML = function(collapsed) { + return settings.services.images[collapsed ? 'collapsed' : 'expanded']; + }, + setRowsCollapsedState = function(toggle, $rows, collapsed) { + if (collapsed) { + $rows.addClass('js-hide'); + } + else { + $rows.removeClass('js-hide'); + } + $(toggle).html(arrowImageHTML(collapsed)); + }; + + $('td.resource-select-all').each(function() { + var resourceName = this.id, + resource = settings.services.resources[this.id], + $rowElements = $('.' + resourceName + '-method'), + $row = $(this.parentElement); + $('div.resource-image', $row) + // Adds group toggling functionality to arrow images. + .bind('click', function () { + resource.collapsed = !resource.collapsed; + setRowsCollapsedState(this, $rowElements, resource.collapsed); + }) + // Set up initial toggle state + .each(function() { + setRowsCollapsedState(this, $rowElements, resource.collapsed); + }); + }); + } + }; + + /** + * Select/deselect all the inner checkboxes when the outer checkboxes are + * selected/deselected. + */ + Drupal.behaviors.resourceSelectAll = { + attach: function (context, settings) { + $('td.resource-select-all').each(function () { + var resourceName = this.id, + methodCheckboxes = $('.' + resourceName + '-method .resource-method-select input[type=checkbox]'), + groupCheckbox = $('').attr('id', this.id + '-select-all'), + // Each time a single-method checkbox is checked or unchecked, make sure + // that the associated group checkbox gets the right state too. + updateGroupCheckbox = function () { + $(groupCheckbox).attr('checked', (methodCheckboxes.filter('[checked]').length == methodCheckboxes.length)); + }; + + // Have the single-method checkboxes follow the group checkbox. + groupCheckbox.bind('change', function () { + methodCheckboxes.attr('checked', $(this).attr('checked')); + }); + + // Have the group checkbox follow the single-method checkboxes. + methodCheckboxes.bind('change', function () { + updateGroupCheckbox(); + }); + + // Initialize status for the group checkbox correctly. + updateGroupCheckbox(); + + $(this).append(groupCheckbox); + }); + } + }; +})(jQuery); \ No newline at end of file diff --git a/openthess/modules/services/plugins/export_ui/services_ctools_export_ui.class.php b/openthess/modules/services/plugins/export_ui/services_ctools_export_ui.class.php new file mode 100644 index 0000000..e6de6df --- /dev/null +++ b/openthess/modules/services/plugins/export_ui/services_ctools_export_ui.class.php @@ -0,0 +1,493 @@ +get_page_title('resources', $item)); + return drupal_get_form('services_edit_form_endpoint_resources', $item); + } + + /** + * Page callback for the server page. + */ + function server_page($js, $input, $item) { + drupal_set_title($this->get_page_title('server', $item)); + return drupal_get_form('services_edit_form_endpoint_server', $item); + } + + + /** + * Page callback for the authentication page. + */ + function authentication_page($js, $input, $item) { + drupal_set_title($this->get_page_title('authentication', $item)); + return drupal_get_form('services_edit_form_endpoint_authentication', $item); + } + + /** + * Page callback for the resource authentication page. + */ + function resource_authentication_page($js, $input, $item) { + drupal_set_title($this->get_page_title('resource_authentication', $item)); + return drupal_get_form('services_edit_form_endpoint_resource_authentication', $item); + } + + // Avoid standard submit of edit form by ctools. + function edit_save_form($form_state) { } + + function set_item_state($state, $js, $input, $item) { + ctools_export_set_object_status($item, $state); + + menu_rebuild(); + if (!$js) { + drupal_goto(ctools_export_ui_plugin_base_path($this->plugin)); + } + else { + return $this->list_page($js, $input); + } + } +} + +/** + * Endpoint authentication configuration form. + */ +function services_edit_form_endpoint_authentication($form, &$form_state) { + list($endpoint) = $form_state['build_info']['args']; + // Loading runtime include as needed by services_authentication_info(). + module_load_include('inc', 'services', 'includes/services.runtime'); + + $auth_modules = module_implements('services_authentication_info'); + + $form['endpoint_object'] = array( + '#type' => 'value', + '#value' => $endpoint, + ); + if (empty($auth_modules)) { + $form['message'] = array( + '#type' => 'item', + '#title' => t('Authentication'), + '#description' => t('No authentication modules are installed, all requests will be anonymous.'), + ); + return $form; + } + if (empty($endpoint->authentication)) { + $form['message'] = array( + '#type' => 'item', + '#title' => t('Authentication'), + '#description' => t('No authentication modules are enabled, all requests will be anonymous.'), + ); + return $form; + } + // Add configuration fieldsets for the authentication modules + foreach ($endpoint->authentication as $module => $settings) { + $info = services_authentication_info($module); + if (empty($info)) { + continue; + } + $form[$module] = array( + '#type' => 'fieldset', + '#title' => isset($info['title']) ? $info['title'] : $module, + '#tree' => TRUE, + ); + + // Append the default settings for the authentication module. + $default_security_settings = services_auth_invoke($module, 'default_security_settings'); + if ($settings == $module && is_array($default_security_settings)) { + $settings = $default_security_settings; + } + // Ask the authentication module for a settings form. + $module_settings_form = services_auth_invoke($module, 'security_settings', $settings, $form_state); + + if (is_array($module_settings_form)) { + $form[$module] += $module_settings_form; + } + else { + $form[$module]['message'] = array( + '#type' => 'item', + '#markup' => t('@module has no settings available.', array('@module' => drupal_ucfirst($module))), + ); + } + } + + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + + return $form; +} + +function services_edit_form_endpoint_authentication_submit($form, $form_state) { + $endpoint = $form_state['values']['endpoint_object']; + + foreach (array_keys($endpoint->authentication) as $module) { + if (isset($form_state['values'][$module])) { + $endpoint->authentication[$module] = $form_state['values'][$module]; + } + } + + drupal_set_message(t('Your authentication options have been saved.')); + services_endpoint_save($endpoint); +} + +function services_edit_form_endpoint_server($form, &$form_state) { + list($endpoint) = $form_state['build_info']['args']; + $servers = services_get_servers(); + $server = !empty($servers[$endpoint->server]) ? $servers[$endpoint->server] : FALSE; + + $form['endpoint_object'] = array( + '#type' => 'value', + '#value' => $endpoint, + ); + + if (!$server) { + $form['message'] = array( + '#type' => 'item', + '#title' => t('Unknown server @name', array('@name' => $endpoint->server)), + '#description' => t('No server matching the one used in the endpoint.'), + ); + } + else if (empty($server['settings'])) { + $form['message'] = array( + '#type' => 'item', + '#title' => t('@name has no settings', array('@name' => $endpoint->server)), + '#description' => t("The server doesn't have any settings that needs to be configured."), + ); + } + else { + $definition = $server['settings']; + + $settings = isset($endpoint->server_settings) ? $endpoint->server_settings : array(); + + if (!empty($definition['file'])) { + call_user_func_array('module_load_include', $definition['file']); + } + + $form[$endpoint->server] = array( + '#type' => 'fieldset', + '#title' => $server['name'], + '#tree' => TRUE, + ); + call_user_func_array($definition['form'], array(&$form[$endpoint->server], $endpoint, $settings)); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + } + + return $form; +} + +function services_edit_form_endpoint_server_submit($form, $form_state) { + $endpoint = $form_state['values']['endpoint_object']; + $servers = services_get_servers(); + $definition = $servers[$endpoint->server]['settings']; + + $values = $form_state['values'][$endpoint->server]; + + // Allow the server to alter the submitted values before they're stored + // as settings. + if (!empty($definition['submit'])) { + if (!empty($definition['file'])) { + call_user_func_array('module_load_include', $definition['file']); + } + $values = call_user_func_array($definition['submit'], array($endpoint, &$values)); + } + + // Store the settings in the endpoint + $endpoint->server_settings = $values; + services_endpoint_save($endpoint); + + drupal_set_message(t('Your server settings have been saved.')); +} + +/** + * services_edit_endpoint_resources function. + * + * Edit Resources endpoint form + * @param object $endpoint + * @return string The form to be displayed + */ +function services_edit_endpoint_resources($endpoint) { + if (!is_object($endpoint)) { + $endpoint = services_endpoint_load($endpoint); + } + if ($endpoint && !empty($endpoint->title)) { + drupal_set_title($endpoint->title); + } + return drupal_get_form('services_edit_form_endpoint_resources', $endpoint); +} + +/** + * services_edit_form_endpoint_resources function. + * + * @param array &$form_state + * @param object $endpoint + * @return Form + */ +function services_edit_form_endpoint_resources($form, &$form_state, $endpoint) { + module_load_include('inc', 'services', 'includes/services.resource_build'); + module_load_include('inc', 'services', 'includes/services.runtime'); + + $form = array(); + $form['endpoint_object'] = array( + '#type' => 'value', + '#value' => $endpoint, + ); + + $form['#attached']['js'] = array( + 'misc/tableselect.js', + drupal_get_path('module', 'services') . '/js/services.admin.js', + ); + + $form['#attached']['css'] = array( + drupal_get_path('module', 'services') . '/css/services.admin.css', + ); + + $ops = array( + 'create' => t('Create'), + 'retrieve' => t('Retrieve'), + 'update' => t('Update'), + 'delete' => t('Delete'), + 'index' => t('Index'), + ); + + // Call _services_build_resources() directly instead of + // services_get_resources to bypass caching. + $resources = _services_build_resources($endpoint->name); + // Sort the resources by the key, which is the string used for grouping each + // resource in theme_services_resource_table(). + ksort($resources); + + $form['instructions'] = array( + '#type' => 'item', + '#title' => t('Resources'), + '#description' => t('Select the resource(s) or methods you would like to enable, and click Save.'), + ); + + $form['resources']= array( + '#theme' => 'services_resource_table', + '#tree' => TRUE, + ); + + $class_names = services_operation_class_info(); + // Collect authentication module info for later use and + // append the default settings for authentication modules + $auth_info = array(); + foreach ($endpoint->authentication as $module => $settings) { + $auth_info[$module] = services_authentication_info($module); + + // Append the default settings for the authentication module. + $default_settings = services_auth_invoke($module, 'default_security_settings'); + if (is_array($default_settings) && is_array($settings)) { + $settings += $default_settings; + } + $endpoint->authentication[$module] = $settings; + } + // Generate the list of methods arranged by resource. + foreach ($resources as $resource_name => $resource) { + $resource_conf = array(); + $resource_key = $resource['key']; + if (isset($endpoint->resources[$resource_key])) { + $resource_conf = $endpoint->resources[$resource_key]; + } + + $res_item = array( + '#collapsed' => TRUE, + ); + $alias = ''; + if (isset($form_state['input'][$resource_key]['alias'])) { + $alias = $form_state['input'][$resource_key]['alias']; + } + elseif (isset($resource_conf['alias'])) { + $alias = $resource_conf['alias']; + } + + $res_item['alias'] = array( + '#type' => 'textfield', + '#default_value' => $alias, + '#size' => 20, + ); + foreach ($class_names as $class => $info) { + if (!empty($resource[$class])) { + $res_item[$class] = array( + '#type' => 'item', + '#title' => $info['title'], + ); + foreach ($resource[$class] as $op_name => $op) { + $description = isset($op['help']) ? $op['help'] : t('No description is available'); + $default_value = 0; + if (isset($resource_conf[$class][$op_name]['enabled'])) { + $default_value = $resource_conf[$class][$op_name]['enabled']; + } + // If any component of a resource is enabled, expand the resource. + if ($default_value) { + $res_item['#collapsed'] = FALSE; + } + $res_item[$class][$op_name] = array( + '#type' => 'item', + '#title' => $op_name, + '#description' => $description, + ); + $res_item[$class][$op_name]['enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Enabled'), + '#default_value' => $default_value, + ); + + $controller_settings = array(); + // Let modules add their own settings. + drupal_alter('controller_settings', $controller_settings); + // Get service update versions. + $update_versions = services_get_update_versions($resource_key, $op_name); + $options = array( + '1.0' => '1.0', + ); + $options = array_merge($options, $update_versions); + $default_api_value = 0; + + if (isset($op['endpoint']) && isset($op['endpoint']['services'])) { + $default_api_value = $op['endpoint']['services']['resource_api_version']; + } + $disabled = (count($options) == 1); + // Add the version information if it has any + if (!$disabled) { + $controller_settings['services'] = array( + '#title' => 'Services', + '#type' => 'item', + 'resource_api_version' => array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $default_api_value, + '#title' => 'Resource API Version', + '#disabled' => $disabled, + ), + ); + } + foreach ($endpoint->authentication as $module => $settings) { + if (isset($endpoint->resources[$resource_key][$class][$op_name]['settings'][$module])) { + $settings = $endpoint->resources[$resource_key][$class][$op_name]['settings'][$module]; + } + $auth_settings = services_auth_invoke($module, 'controller_settings', $settings, $op, $endpoint->authentication[$module], $class, $op_name); + if (is_array($auth_settings)) { + $auth_settings = array( + '#title' => $auth_info[$module]['title'], + '#type' => 'item', + ) + $auth_settings; + $controller_settings[$module] = $auth_settings; + $disabled = FALSE; + } + } + if (!$disabled) { + $res_item[$class][$op_name]['settings'] = $controller_settings; + } + } + } + } + $form['resources'][$resource_key] = $res_item; + } + $form['save'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + return $form; +} + +/** + * services_edit_form_endpoint_resources_validate function. + * + * @param array $form + * @param array $form_state + * @return void + */ +function services_edit_form_endpoint_resources_validate($form, $form_state) { + $input = $form_state['values']; + + // Validate aliases. + foreach ($input['resources'] as $resource_name => $resource) { + if (!empty($resource['alias']) && !preg_match('/^[a-z-_]+$/', $resource['alias'])) { + // Still this doesn't highlight needed form element. + form_set_error("resources][{$resource_name}][alias", t("The alias for the !name resource may only contain lower case a-z, underscores and dashes.", array( + '!name' => $resource_name, + ))); + } + } +} + +/** + * Resources form submit function. + * + * @param array $form + * @param array $form_state + * @return void + */ +function services_edit_form_endpoint_resources_submit($form, $form_state) { + $endpoint = $form_state['values']['endpoint_object']; + $resources = $form_state['input']['resources']; + $class_names = services_operation_class_info(); + // Iterate over the resources, its operation classes and operations. + // The main purpose is to remove empty configuration for disabled elements. + foreach ($resources as $resource_name => $resource) { + if (empty($resource['alias'])) { + unset($resource['alias']); + } + foreach ($class_names as $class_name => $info) { + if (!empty($resource[$class_name])) { + foreach ($resource[$class_name] as $op_name => $op) { + // Remove the operation if it has been disabled. + if (!$op['enabled']) { + unset($resource[$class_name][$op_name]); + } + } + } + // Remove the operation class element if it doesn't + // have any enabled operations. + if (empty($resource[$class_name])) { + unset($resource[$class_name]); + } + } + // Remove the resource if it doesn't have any properties. + if (empty($resource)) { + unset($resources[$resource_name]); + } + // Add the processed resource if it does. + else { + $resources[$resource_name] = $resource; + } + } + $endpoint->resources = $resources; + services_endpoint_save($endpoint); + drupal_set_message('Resources have been saved'); +} + +/** + * Returns the updates for a given resource method. + * + * @param $resource + * A resource name. + * @param $method + * A method name. + * @return + * an array with the major and minor api versions + */ +function services_get_update_versions($resource, $method) { + $versions = array(); + $updates = services_get_updates(); + if (isset($updates[$resource][$method]) && is_array($updates[$resource][$method])) { + foreach ($updates[$resource][$method] as $update) { + extract($update); + $value = $major . '.' . $minor; + $versions[$value] = $value; + } + } + return $versions; +} diff --git a/openthess/modules/services/plugins/export_ui/services_ctools_export_ui.inc b/openthess/modules/services/plugins/export_ui/services_ctools_export_ui.inc new file mode 100644 index 0000000..01c207c --- /dev/null +++ b/openthess/modules/services/plugins/export_ui/services_ctools_export_ui.inc @@ -0,0 +1,231 @@ + 'services_endpoint', + 'menu' => array( + 'menu item' => 'services', + 'menu description' => 'Manage Services', + // Add services specific own menu callbacks. + 'items' => array( + 'resources' => array( + 'path' => 'list/%ctools_export_ui/resources', + 'title' => 'Resources', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array('services_ctools_export_ui', 'resources', 4), + 'load arguments' => array('services_ctools_export_ui'), + 'access arguments' => array('administer services'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -2, + ), + 'server' => array( + 'path' => 'list/%ctools_export_ui/server', + 'title' => 'Server', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array('services_ctools_export_ui', 'server', 4), + 'load arguments' => array('services_ctools_export_ui'), + 'access arguments' => array('administer services'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -4, + ), + 'authentication' => array( + 'path' => 'list/%ctools_export_ui/authentication', + 'title' => 'Authentication', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array('services_ctools_export_ui', 'authentication', 4), + 'load arguments' => array('services_ctools_export_ui'), + 'access arguments' => array('administer services'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -3, + ), + ), + ), + // Add our custom operations. + 'allowed operations' => array( + 'resources' => array('title' => t('Edit Resources')), + 'server' => array('title' => t('Edit Server')), + 'authentication' => array('title' => t('Edit Authentication')), + ), + 'form' => array( + 'settings' => 'services_ctools_export_ui_form', + 'validate' => 'services_ctools_export_ui_form_validate', + 'submit' => 'services_ctools_export_ui_form_submit', + ), + 'handler' => array( + 'class' => 'services_ctools_export_ui', + 'parent' => 'ctools_export_ui', + ), + + 'title' => t('Services'), + + 'title singular' => t('endpoint'), + 'title plural' => t('endpoints'), + 'title singular proper' => t('Endpoint'), + 'title plural proper' => t('Endpoints'), +); + +/** + * Form to edit the settings of an endpoint. + */ +function services_ctools_export_ui_form(&$form, &$form_state) { + // Loading runtime include as needed by services_auth_info(). + module_load_include('inc', 'services', 'includes/services.runtime'); + $endpoint = $form_state['item']; + + $form['info']['name'] = array_merge($form['info']['name'], array( + '#title' => t('Machine-readable name of the endpoint'), + '#type' => 'machine_name', + '#description' => t('The endpoint name can only consist of lowercase letters, underscores, and numbers.'), + '#machine_name' => array( + 'exists' => 'services_ctools_export_ui_form_machine_name_exists', + ), + )); + + $form['eid'] = array( + '#type' => 'value', + '#value' => isset($endpoint->eid) ? $endpoint->eid : '', + ); + + $form['endpoint_object'] = array( + '#type' => 'value', + '#value' => $endpoint, + ); + + $servers = services_get_servers(); + $server_opts = array( + '' => t('-- Select a server'), + ); + foreach ($servers as $server => $info) { + $server_opts[$server] = $info['name']; + } + $form['server'] = array( + '#type' => 'select', + '#options' => $server_opts, + '#default_value' => $endpoint->server, + '#title' => t('Server'), + '#description' => t('Select a the server that should be used to handle requests to this endpoint.'), + '#required' => TRUE, + ); + + $form['path'] = array( + '#type' => 'textfield', + '#size' => 24, + '#maxlength' => 255, + '#default_value' => $endpoint->path, + '#title' => t('Path to endpoint'), + '#required' => TRUE, + ); + + $form['debug'] = array( + '#type' => 'checkbox', + '#default_value' => $endpoint->debug, + '#title' => t('Debug mode enabled'), + '#description' => t('Useful for developers. Do not enable on production environments'), + '#required' => FALSE, + ); + $auth_modules = module_implements('services_authentication_info'); + + if (!empty($auth_modules)) { + $auth_options = array(); + foreach ($auth_modules as $module) { + $info = services_authentication_info($module); + $auth_options[$module] = $info['title']; + } + $default_values = array(); + foreach ($endpoint->authentication as $auth_module => $settings) { + if (!empty($settings)) { + $default_values[] = $auth_module; + } + } + + $form['authentication'] = array( + '#type' => 'checkboxes', + '#options' => $auth_options, + '#default_value' => $default_values, + '#title' => t('Authentication'), + '#description' => t('Choose which authentication schemes that should ' . + 'be used with your endpoint. If no authentication method is selected ' . + 'all requests will be done by an anonymous user.'), + ); + } + else { + $form['authentication'] = array( + '#type' => 'item', + '#title' => t('Authentication'), + '#description' => t('No authentication modules are installed, all ' . + 'requests will be anonymous.'), + ); + } + + return $form; + +} + +/** + * Validate submission of the preset edit form. + */ +function services_ctools_export_ui_form_validate(&$form, &$form_state) { + // Validate path. + $query = db_select('services_endpoint', 'e'); + $query->addField('e', 'eid'); + $query->condition('path', $form_state['values']['path']); + + if (!empty($form_state['values']['eid']) && is_numeric($form_state['values']['eid'])) { + $query->condition('eid', $form_state['values']['eid'], '!='); + } + + $res = $query->execute()->fetchField(); + if (!empty($res)) { + form_error($form['path'], t('Endpoint path must be unique.')); + } +} + +/** + * Endpoint name check whether this machine name already exists. + */ +function services_ctools_export_ui_form_machine_name_exists($value) { + // Validate Name. +// $query = db_select('services_endpoint', 'e'); +// $query->addField('e', 'eid'); +// $query->condition('name', $value); + + $result = db_query('SELECT eid FROM {services_endpoint} WHERE name = :name', array(':name' => $value))->fetchField(); + return !empty($result); +} + +/** + * Submit handler for endpoint. + */ +function services_ctools_export_ui_form_submit(&$form, &$form_state) { + $endpoint = $form_state['values']['endpoint_object']; + + $endpoint->name = $form_state['values']['name']; + $endpoint->server = $form_state['values']['server']; + $endpoint->path = $form_state['values']['path']; + $endpoint->debug = $form_state['values']['debug']; + + // Set the authentication modules, and preserve the settings for modules + // that already exist. + $auth = array(); + if (isset($form_state['values']['authentication'])) { + foreach (array_keys($form_state['values']['authentication']) as $module) { + //if module's checkbox is checked, add to empty + $auth_module = $form_state['values']['authentication'][$module]; + if ($module === $auth_module) { + //If existing settings are set, preserve them + if (isset($endpoint->authentication[$module]) && is_array($endpoint->authentication[$module]) && !empty($endpoint->authentication[$module])) { + $auth[$module] = $endpoint->authentication[$module]; + } + else { + $auth[$module] = $auth_module; + } + } + elseif ($auth_module == 0) { + unset($auth[$module]); + } + } + } + $endpoint->authentication = $auth; + services_endpoint_save($endpoint); +} \ No newline at end of file diff --git a/openthess/modules/services/resources/comment_resource.inc b/openthess/modules/services/resources/comment_resource.inc new file mode 100644 index 0000000..84b71a7 --- /dev/null +++ b/openthess/modules/services/resources/comment_resource.inc @@ -0,0 +1,402 @@ + array( + 'operations' => array( + 'create' => array( + 'help' => 'Create a comment', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/comment_resource'), + 'callback' => '_comment_resource_create', + 'access callback' => '_comment_resource_access', + 'access arguments' => array('create'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'comment', + 'type' => 'array', + 'description' => 'The comment object', + 'source' => 'data', + 'optional' => FALSE, + ), + ), + ), + + 'retrieve' => array( + 'help' => 'Retrieve a comment', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/comment_resource'), + 'callback' => '_comment_resource_retrieve', + 'access callback' => '_comment_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'cid', + 'type' => 'int', + 'description' => 'The cid of the comment to retrieve.', + 'source' => array('path' => '0'), + 'optional' => FALSE, + ), + ), + ), + + 'update' => array( + 'help' => 'Update a comment', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/comment_resource'), + 'callback' => '_comment_resource_update', + 'access callback' => '_comment_resource_access', + 'access arguments' => array('edit'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'cid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The unique identifier for this comment.', + ), + array( + 'name' => 'data', + 'type' => 'array', + 'description' => 'The comment object with updated information', + 'source' => 'data', + 'optional' => FALSE, + ), + ), + ), + + 'delete' => array( + 'help' => 'Delete a comment', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/comment_resource'), + 'callback' => '_comment_resource_delete', + 'access callback' => '_comment_resource_access', + 'access arguments' => array('edit'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'cid', + 'type' => 'int', + 'description' => 'The id of the comment to delete', + 'source' => array('path' => '0'), + 'optional' => FALSE, + ), + ), + ), + 'index' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/comment_resource'), + 'callback' => '_comment_resource_index', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_comment_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access callback' => '_comment_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + ), + ), + 'actions' => array( + 'countAll' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/comment_resource'), + 'help' => t('Return number of comments on a given node.'), + 'access callback' => '_comment_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'callback' => '_comment_resource_count_all', + 'args' => array( + array( + 'name' => 'nid', + 'type' => 'int', + 'description' => t('The node id to count all comments.'), + 'source' => array('data' => 'nid'), + 'optional' => FALSE, + ), + ), + ), + 'countNew' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/comment_resource'), + 'help' => t('Returns number of new comments on a given node since a given timestamp.'), + 'access callback' => '_comment_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'callback' => '_comment_resource_count_new', + 'args' => array( + array( + 'name' => 'nid', + 'type' => 'int', + 'description' => t('The node id to load comments for.'), + 'source' => array('data' => 'nid'), + 'optional' => FALSE, + ), + array( + 'name' => 'since', + 'type' => 'int', + 'optional' => TRUE, + 'description' => t('Timestamp to count from (defaults to time of last user acces to node).'), + 'source' => array('data' => 'since'), + 'optional' => TRUE, + 'default value' => 0, + ), + ), + ), + ), + ), + ); +} + +/** + * Adds a new comment to a node and returns the cid. + * + * @param $comment + * An object as would be returned from comment_load(). + * @return + * Unique identifier for the comment (cid) or errors if there was a problem. + */ + +function _comment_resource_create($comment) { + // Adds backwards compatability with regression fixed in #1083242 + $comment = _services_arg_value($comment, 'comment'); + + if (empty($comment['nid'])) { + return services_error(t('A nid must be provided')); + } + + $form_state['values'] = $comment; + $form_state['values']['op'] = variable_get('services_comment_save_button_resource_create', t('Save')); + + $comment_empty = array( + 'nid' => $comment['nid'], + 'cid' => NULL, + ); + + // If a pid is provide use it + if (!empty($comment['pid'])) { + $comment_empty['pid'] = $comment['pid']; + } + + $comment_empty = (object) $comment_empty; + $form_state['programmed_bypass_access_check'] = FALSE; + $ret = drupal_form_submit('comment_form', $form_state, $comment_empty); + + // Error if needed. + if ($errors = form_get_errors()) { + return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); + } + + $comment = $form_state['comment']; + + return array( + 'cid' => $comment->cid, + 'uri' => services_resource_uri(array('comment', $comment->cid)), + ); +} + +/** + * Returns a specified comment + * + * @param $cid + * Unique identifier for the specified comment + * @return + * The comment object + */ +function _comment_resource_retrieve($cid) { + return comment_load($cid); +} + +/** + * Updates a comment and returns the cid. + * + * @param $cid + * Unique identifier for this comment. + * @param $comment + * An object as would be returned from comment_load(). + * @return + * Unique identifier for the comment (cid) or FALSE if there was a problem. + */ +function _comment_resource_update($cid, $comment) { + // Adds backwards compatability with regression fixed in #1083242 + $comment = _services_arg_value($comment, 'data'); + $comment['cid'] = $cid; + + $old_comment = comment_load($cid); + if (empty($old_comment)) { + return services_error(t('Comment @cid not found', array('@cid' => $cid)), 404); + } + // Setup form_state. + $form_state = array(); + $form_state['values'] = $comment; + $form_state['values']['op'] = variable_get('services_comment_save_button_resource_update', t('Save')); + $form_state['comment'] = $old_comment; + $form_state['programmed_bypass_access_check'] = FALSE; + + drupal_form_submit('comment_form', $form_state, $old_comment); + + if ($errors = form_get_errors()) { + return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); + } + + return $cid; +} + +/** + * Delete a comment. + * + * @param $cid + * Unique identifier of the comment to delete. + * @return + * True. + */ +function _comment_resource_delete($cid) { + // Load in the required includes for comment_delete. + module_load_include('inc', 'comment', 'comment.admin'); + + // The following is from comment_confirm_delete_submit in comment.admin.inc + $comment = comment_load($cid); + if (empty($comment)) { + return services_error(t('There is no comment found with id @cid', array('@cid' => $cid)), 404); + } + + // Delete comment and its replies. + comment_delete($cid); + + // Clear the cache so an anonymous user sees that his comment was deleted. + cache_clear_all(); + return TRUE; +} +/** + * Return an array of optionally paged cids baed on a set of criteria. + * + * An example request might look like + * + * http://domain/endpoint/comment?fields=cid,nid¶meters[nid]=7¶meters[uid]=2 + * + * This would return an array of objects with only cid and nid defined, where + * nid = 7 and uid = 1. + * + * @param $page + * Page number of results to return (in pages of 20). + * @param $fields + * The fields you want returned. + * @param $parameters + * An array containing fields and values used to build a sql WHERE clause + * indicating items to retrieve. + * @param $page_size + * Integer number of items to be returned. + * @return + * An array of comment objects. + * + * @see _node_resource_index() for more notes + **/ +function _comment_resource_index($page, $fields, $parameters, $page_size) { + $comment_select = db_select('comment', 't') + ->orderBy('created', 'DESC'); + + services_resource_build_index_query($comment_select, $page, $fields, $parameters, $page_size, 'comment'); + + if (!user_access('administer comments')) { + $comment_select->condition('status', COMMENT_PUBLISHED); + } + + $results = services_resource_execute_index_query($comment_select); + + return services_resource_build_index_list($results, 'comment', 'cid'); +} + +/** + * Returns the number of comments on a given node id. + * + * @param $nid + * Unique identifier for the specified node. + * @return + * Number of comments that node has. + */ +function _comment_resource_count_all($nid) { + $node = node_load($nid); + return $node->comment_count; +} + +/** + * Returns the number of new comments on a given node id since timestamp. + * + * @param $nid + * Unique identifier for the specified node. + * @param $since + * Timestamp to indicate what nodes are new. Defaults to time of last user acces to node. + * @return + * Number of comments that node has. + */ +function _comment_resource_count_new($nid, $since) { + return comment_num_new($nid, $since); +} + +/** + * Access check callback for comment controllers. + */ +function _comment_resource_access($op = 'view', $args = array()) { + // Adds backwards compatability with regression fixed in #1083242 + if (isset($args[0])) { + $args[0] = _services_access_value($args[0], array('comment', 'data')); + } + + if ($op == 'create') { + $comment = (object)$args[0]; + } + else { + $comment = comment_load($args[0]); + } + if(isset($comment->nid)) { + $node = node_load($comment->nid); + if($op == 'create' && !$node->nid) { + return services_error(t('Node nid: @nid does not exist.', array('@nid' => $comment->nid)), 406); + } + } + if (user_access('administer comments')) { + return TRUE; + } + switch ($op) { + case 'view': + // Check if the user has access to comments + return user_access('access comments'); + case 'delete': + return user_access('administer comments'); + case 'edit': + return comment_access('edit', $comment); + case 'create': + // Check if the user may post comments, node has comments enabled + // and that the user has access to the node. + return user_access('post comments') && ($node->comment == COMMENT_NODE_OPEN); + } +} diff --git a/openthess/modules/services/resources/file_resource.inc b/openthess/modules/services/resources/file_resource.inc new file mode 100644 index 0000000..a4bbb85 --- /dev/null +++ b/openthess/modules/services/resources/file_resource.inc @@ -0,0 +1,371 @@ + array( + 'operations' => array( + 'create' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/file_resource'), + 'help' => 'Create a file with base64 encoded data', + 'callback' => '_file_resource_create', + 'access callback' => '_file_resource_access', + 'access arguments' => array('create'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'file', + 'type' => 'array', + 'description' => t('An array representing a file.'), + 'source' => 'data', + 'optional' => FALSE, + ), + ), + ), + 'retrieve' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/file_resource'), + 'help' => 'Retrieve a file', + 'callback' => '_file_resource_retrieve', + 'access callback' => '_file_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'fid', + 'type' => 'int', + 'description' => 'The fid of the file to retrieve.', + 'source' => array('path' => '0'), + 'optional' => FALSE, + ), + array( + 'name' => 'file_contents', + 'type' => 'int', + 'description' => t('To return file contents or not.'), + 'source' => array('param' => 'file_contents'), + 'default value' => TRUE, + 'optional' => TRUE, + ), + array( + 'name' => 'image_styles', + 'type' => 'int', + 'description' => t('To return image styles or not.'), + 'source' => array('param' => 'image_styles'), + 'default value' => FALSE, + 'optional' => TRUE, + ), + ), + ), + 'delete' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/file_resource'), + 'help' => 'Delete a file', + 'callback' => '_file_resource_delete', + 'access callback' => '_file_resource_access', + 'access arguments' => array('delete'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'cid', + 'type' => 'int', + 'description' => 'The id of the file to delete', + 'source' => array('path' => '0'), + 'optional' => FALSE, + ), + ), + ), + 'index' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/file_resource'), + 'callback' => '_file_resource_index', + 'help' => 'List all files', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_file_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access callback' => '_file_resource_access', + 'access arguments' => array('index'), + 'access arguments append' => TRUE, + ), + ), + 'actions' => array( + 'create_raw' => array( + 'help' => 'Create a file with raw data.', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/file_resource'), + 'callback' => '_file_resource_create_raw', + 'access callback' => '_file_resource_access', + 'access arguments' => array('create_raw'), + 'access arguments append' => TRUE, + ), + ), + ), + ); +} + +/** + * Adds a new file and returns the fid. + * + * @param $file + * An array as representing the file with a base64 encoded $file['file'] + * @return + * Unique identifier for the file (fid) or errors if there was a problem. + */ +function _file_resource_create($file) { + // Adds backwards compatability with regression fixed in #1083242 + // $file['file'] can be base64 encoded file so we check whether it is + // file array or file data. + $file = _services_arg_value($file, 'file'); + + // If the file data or filename is empty then bail. + if (!isset($file['file']) || empty($file['filename'])) { + return services_error(t("Missing data the file upload can not be completed"), 500); + } + + // Get the directory name for the location of the file: + if (empty($file['filepath'])) { + $file['filepath'] = file_default_scheme() . '://' . $file['filename']; + } + $dir = drupal_dirname($file['filepath']); + // Build the destination folder tree if it doesn't already exists. + if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY)) { + return services_error(t("Could not create destination directory for file."), 500); + } + + // Rename potentially executable files, to help prevent exploits. + if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file['filename']) && (drupal_substr($file['filename'], -4) != '.txt')) { + $file['filepath'] .= '.txt'; + $file['filename'] .= '.txt'; + } + + // Write the file + if (!$file_saved = file_save_data(base64_decode($file['file']), $file['filepath'])) { + return services_error(t("Could not write file to destination"), 500); + } + + if (isset($file['status']) && $file['status'] == 0) { + // Save as temporary file. + $file_saved->status = 0; + file_save($file_saved); + } + else { + // Required to be able to reference this file. + file_usage_add($file_saved, 'services', 'files', $file_saved->fid); + } + + return array( + 'fid' => $file_saved->fid, + 'uri' => services_resource_uri(array('file', $file_saved->fid)), + ); +} +/** + * Adds new files and returns the files array. + * + * @return + * Array of file objects with URIS to access them + */ +function _file_resource_create_raw() { + $validators = array( + 'file_validate_extensions' => array(), + 'file_validate_size' => array(), + ); + + $files = array(); + foreach ($_FILES['files']['name'] as $field_name => $file_name) { + $file = file_save_upload($field_name, $validators, file_default_scheme() . "://"); + + if (!empty($file->fid)) { + // Change the file status from temporary to permanent. + $file->status = FILE_STATUS_PERMANENT; + file_save($file); + + // Required to be able to reference this file. + file_usage_add($file, 'services', 'files', $file->fid); + + $files[] = array( + 'fid' => $file->fid, + 'uri' => services_resource_uri(array('file', $file->fid)), + ); + } + else { + return services_error(t('An unknown error occured'), 500); + } + } + return $files; +} +/** + * Get a given file + * + * @param $fid + * Number. File ID + * @param $include_file_contents + * Bool Whether or not to include the base64_encoded version of the file. + * @param $get_image_style + * Bool Whether or not to provide image style paths. + * @return + * The file + */ +function _file_resource_retrieve($fid, $include_file_contents, $get_image_style) { + if ($file = file_load($fid)) { + $filepath = $file->uri; + + // Convert the uri to the external url path provided by the stream wrapper. + $file->uri_full = file_create_url($file->uri); + + // Provide a path in the form sample/test.txt. + $file->target_uri = file_uri_target($file->uri); + + if ($include_file_contents) { + $file->file = base64_encode(file_get_contents(drupal_realpath($filepath))); + } + + $file->image_styles = array(); + // Add image style information if available. + if ($get_image_style) { + foreach (image_styles() as $style) { + $style_name = $style['name']; + $file->image_styles[$style_name] = image_style_url($style_name, $file->uri); + } + } + return $file; + } +} + +/** + * Delete a file. + * + * @param $fid + * Unique identifier of the file to delete. + * @return bool + * Whether or not the delete was successful. + */ +function _file_resource_delete($fid) { + if ($file = file_load($fid)) { + file_usage_delete($file, 'services'); + return file_delete($file); + } + return FALSE; +} + +/** + * Return an array of optionally paged fids baed on a set of criteria. + * + * An example request might look like + * + * http://domain/endpoint/file?fields=fid,filename¶meters[fid]=7¶meters[uid]=1 + * + * This would return an array of objects with only fid and filename defined, where + * fid = 7 and uid = 1. + * + * @param $page + * Page number of results to return (in pages of 20). + * @param $fields + * The fields you want returned. + * @param $parameters + * An array containing fields and values used to build a sql WHERE clause + * indicating items to retrieve. + * @param $page_size + * Integer number of items to be returned. + * @return + * An array of file objects. + * + * @see _node_resource_index() for more notes + **/ +function _file_resource_index($page, $fields, $parameters, $page_size) { + $file_select = db_select('file_managed', 't') + ->orderBy('timestamp', 'DESC'); + + services_resource_build_index_query($file_select, $page, $fields, $parameters, $page_size, 'file'); + + $results = services_resource_execute_index_query($file_select); + + // Put together array of matching files to return. + return services_resource_build_index_list($results, 'file', 'fid'); +} + +/** + * Access check callback for file controllers. + */ +function _file_resource_access($op = 'view', $args = array()) { + // Adds backwards compatability with regression fixed in #1083242 + if (isset($args[0])) { + $args[0] = _services_access_value($args[0], 'file'); + } + + global $user; + + if (($op != 'create' && $op != 'create_raw') && $op != 'index') { + $file = file_load($args[0]); + } else if ($op == 'create' && $op != 'create_raw') { + $file = (object)$args[0]; + } + if (empty($file) && $op != 'index' && ($op != 'create' && $op != 'create_raw')) { + return services_error(t('There is no file with ID @fid', array('@fid' => $args[0])), 406); + } + switch ($op) { + case 'view': + if (user_access('get any binary files')) { + return TRUE; + } + return $file->uid == $user->uid && user_access('get own binary files'); + break; + case 'create': + case 'create_raw': + return user_access('save file information'); + case 'delete': + return $file->uid == $user->uid && user_access('save file information'); + break; + case 'index': + if (user_access('get any binary files')) { + return TRUE; + } + } + + return FALSE; +} + +function _file_resource_node_access($op = 'view', $args = array()) { + global $user; + if (user_access('get any binary files')) { + return TRUE; + } + elseif ($node = node_load($args[0])) { + return $node->uid == $user->uid && user_access('get own binary files'); + } + return FALSE; +} diff --git a/openthess/modules/services/resources/node_resource.inc b/openthess/modules/services/resources/node_resource.inc new file mode 100644 index 0000000..d957bef --- /dev/null +++ b/openthess/modules/services/resources/node_resource.inc @@ -0,0 +1,765 @@ + array( + 'operations' => array( + 'retrieve' => array( + 'help' => 'Retrieve a node', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_retrieve', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to retrieve', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + ), + 'create' => array( + 'help' => 'Create a node', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_create', + 'args' => array( + array( + 'name' => 'node', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The node data to create', + 'type' => 'array', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('create'), + 'access arguments append' => TRUE, + ), + 'update' => array( + 'help' => 'Update a node', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_update', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to update', + ), + array( + 'name' => 'node', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The node data to update', + 'type' => 'array', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('update'), + 'access arguments append' => TRUE, + ), + 'delete' => array( + 'help' => t('Delete a node'), + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_delete', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to delete', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('delete'), + 'access arguments append' => TRUE, + ), + 'index' => array( + 'help' => 'List all nodes', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_index', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters array', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_node_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access arguments' => array('access content'), + ), + ), + 'targeted_actions' => array( + 'attach_file' => array( + 'help' => 'Upload and attach file(s) to a node. POST multipart/form-data to node/123/attach_file', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_attach_file', + 'access callback' => '_node_resource_access', + 'access arguments' => array('update'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to attach a file to', + ), + array( + 'name' => 'field_name', + 'optional' => FALSE, + 'source' => array('data' => 'field_name'), + 'description' => 'The file field name', + 'type' => 'string', + ), + array( + 'name' => 'attach', + 'optional' => TRUE, + 'source' => array('data' => 'attach'), + 'description' => 'Attach the file(s) to the node. If FALSE, this clears ALL files attached, and attaches the files', + 'type' => 'int', + 'default value' => TRUE, + ), + array( + 'name' => 'field_values', + 'optional' => TRUE, + 'source' => array('data' => 'field_values'), + 'description' => 'The extra field values', + 'type' => 'array', + 'default value' => array(), + ), + ), + ), + ), + 'relationships' => array( + 'files' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'help' => 'This method returns files associated with a node.', + 'access callback' => '_node_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'callback' => '_node_resource_load_node_files', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node whose files we are getting', + ), + array( + 'name' => 'file_contents', + 'type' => 'int', + 'description' => t('To return file contents or not.'), + 'source' => array('path' => 2), + 'optional' => TRUE, + 'default value' => TRUE, + ), + array( + 'name' => 'image_styles', + 'type' => 'int', + 'description' => t('To return image styles or not.'), + 'source' => array('path' => 3), + 'optional' => TRUE, + 'default value' => FALSE, + ), + ), + ), + ), + ), + ); + if (module_exists('comment')) { + $comments = array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'help' => 'This method returns the number of new comments on a given node.', + 'access callback' => 'user_access', + 'access arguments' => array('access comments'), + 'access arguments append' => FALSE, + 'callback' => '_node_resource_load_node_comments', + 'args' => array( + array( + 'name' => 'nid', + 'type' => 'int', + 'description' => t('The node id to load comments for.'), + 'source' => array('path' => 0), + 'optional' => FALSE, + ), + array( + 'name' => 'count', + 'type' => 'int', + 'description' => t('Number of comments to load.'), + 'source' => array('param' => 'count'), + 'optional' => TRUE, + ), + array( + 'name' => 'offset', + 'type' => 'int', + 'description' => t('If count is set to non-zero value, you can pass also non-zero value for start. For example to get comments from 5 to 15, pass count=10 and start=5.'), + 'source' => array('param' => 'offset'), + 'optional' => TRUE, + ), + ), + ); + $node_resource['node']['relationships']['comments'] = $comments; + } + return $node_resource; +} + +/** + * Returns the results of a node_load() for the specified node. + * + * This returned node may optionally take content_permissions settings into + * account, based on a configuration setting. + * + * @param $nid + * NID of the node we want to return. + * @return + * Node object or FALSE if not found. + * + * @see node_load() + */ +function _node_resource_retrieve($nid) { + $node = node_load($nid); + + if ($node) { + $uri = entity_uri('node', $node); + $node->path = url($uri['path'], array('absolute' => TRUE)); + // Unset uri as it has complete entity and this + // cause never ending recursion in rendering. + unset($node->uri); + } + return $node; +} + +/** + * Creates a new node based on submitted values. + * + * Note that this function uses drupal_form_submit() to create new nodes, + * which may require very specific formatting. The full implications of this + * are beyond the scope of this comment block. The Googles are your friend. + * + * @param $node + * Array representing the attributes a node edit form would submit. + * @return + * An associative array contained the new node's nid and, if applicable, + * the fully qualified URI to this resource. + * + * @see drupal_form_submit() + */ +function _node_resource_create($node) { + global $user; + // Adds backwards compatability with regression fixed in #1083242 + $node = _services_arg_value($node, 'node'); + if (!isset($node['name'])) { + // Assign username to the node from $user created at auth step. + if (isset($user->name)) { + $node['name'] = $user->name; + } + } + if(!isset($node['language'])) { + $node['language'] = LANGUAGE_NONE; + } + // Validate the node. If there is validation error Exception will be thrown + // so code below won't be executed. + _node_resource_validate_type($node); + + // Load the required includes for drupal_form_submit + module_load_include('inc', 'node', 'node.pages'); + + $node_type = $node['type']; + + // Setup form_state + $form_state = array(); + $form_state['values'] = $node; + $form_state['values']['op'] = variable_get('services_node_save_button_' . $node_type . '_resource_create', t('Save')); + $form_state['programmed_bypass_access_check'] = FALSE; + + // Build a stub node object for the form in a similar way as node_add() does, + // but always make the node author default to the current user (if the user + // has permission to change it, $form_state['values'] will override this + // default when the form is submitted). + $stub_node = (object) array_intersect_key($node, array_flip(array('type', 'language'))); + $stub_node->name = $user->name; + drupal_form_submit($node_type . '_node_form', $form_state, (object)$stub_node); + + if ($errors = form_get_errors()) { + return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); + } + // Fetch $nid out of $form_state + $nid = $form_state['nid']; + // Only add the URI for servers that support it. + $node = array('nid' => $nid); + if ($uri = services_resource_uri(array('node', $nid))) { + $node['uri'] = $uri; + } + return $node; +} + +/* + * Helper function to validate node type information. + * + * @param $node + * Array representing the attributes a node edit form would submit. + */ +function _node_resource_validate_type($node) { + if (!isset($node['type'])) { + return services_error(t('Missing node type'), 406); + } + // Wanted to return a graceful error instead of a blank nid, this should + // allow for that. + $types = node_type_get_types(); + $node_type = $node['type']; + if (!isset($types[$node_type])) { + return services_error(t('Node type @type does not exist.', array('@type' => $node_type)), 406); + } + $allowed_node_types = variable_get('services_allowed_create_content_types', $types); + if (!isset($allowed_node_types[$node_type])) { + return services_error(t("This node type @type can't be processed via services", array('@type' => $node_type)), 406); + } +} + +/** + * Updates a new node based on submitted values. + * + * Note that this function uses drupal_form_submit() to create new nodes, + * which may require very specific formatting. The full implications of this + * are beyond the scope of this comment block. The Googles are your friend. + * + * @param $nid + * Node ID of the node we're editing. + * @param $node + * Array representing the attributes a node edit form would submit. + * @return + * The node's nid. + * + * @see drupal_form_submit() + */ +function _node_resource_update($nid, $node) { + // Adds backwards compatability with regression fixed in #1083242 + $node = _services_arg_value($node, 'node'); + + $node['nid'] = $nid; + + $old_node = node_load($nid); + if (empty($old_node->nid)) { + return services_error(t('Node @nid not found', array('@nid' => $old_node->nid)), 404); + } + + // If no type is provided use the existing node type. + if (empty($node['type'])) { + $node['type'] = $old_node->type; + } + elseif ($node['type'] != $old_node->type) { + // Node types cannot be changed once they are created. + return services_error(t('Node type cannot be changed'), 406); + } + + // Validate the node. If there is validation error Exception will be thrown + // so code below won't be executed. + _node_resource_validate_type($node); + + // Load the required includes for drupal_form_submit + module_load_include('inc', 'node', 'node.pages'); + + $node_type = $node['type']; + node_object_prepare($old_node); + + // Setup form_state. + $form_state = array(); + $form_state['values'] = $node; + $form_state['values']['op'] = variable_get('services_node_save_button_' . $node_type . '_resource_update', t('Save')); + $form_state['node'] = $old_node; + $form_state['programmed_bypass_access_check'] = FALSE; + drupal_form_submit($node_type . '_node_form', $form_state, $old_node); + + if ($errors = form_get_errors()) { + return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); + } + + $node = array('nid' => $nid); + if ($uri = services_resource_uri(array('node', $nid))) { + $node['uri'] = $uri; + } + return $node; +} + +/** + * Delete a node given its nid. + * + * @param int $nid + * Node ID of the node we're deleting. + * @return bool + * Always returns true. + */ +function _node_resource_delete($nid) { + node_delete($nid); + return TRUE; +} + +/** + * Return an array of optionally paged nids baed on a set of criteria. + * + * An example request might look like + * + * http://domain/endpoint/node?fields=nid,vid¶meters[nid]=7¶meters[uid]=1 + * + * This would return an array of objects with only nid and vid defined, where + * nid = 7 and uid = 1. + * + * @param $page + * Page number of results to return (in pages of 20). + * @param $fields + * The fields you want returned. + * @param $parameters + * An array containing fields and values used to build a sql WHERE clause + * indicating items to retrieve. + * @param $page_size + * Integer number of items to be returned. + * @return + * An array of node objects. + * + * @todo + * Evaluate the functionality here in general. Particularly around + * - Do we need fields at all? Should this just return full nodes? + * - Is there an easier syntax we can define which can make the urls + * for index requests more straightforward? + */ +function _node_resource_index($page, $fields, $parameters, $page_size) { + module_load_include('inc', 'services', 'services.module'); + $node_select = db_select('node', 't') + ->addTag('node_access') + ->orderBy('sticky', 'DESC') + ->orderBy('created', 'DESC'); + + services_resource_build_index_query($node_select, $page, $fields, $parameters, $page_size, 'node'); + + if (!user_access('administer nodes')) { + $node_select->condition('status', 1); + } + + $results = services_resource_execute_index_query($node_select); + + return services_resource_build_index_list($results, 'node', 'nid'); +} + +/** + * Determine whether the current user can access a node resource. + * + * @param $op + * One of view, update, create, delete per node_access(). + * @param $args + * Resource arguments passed through from the original request. + * @return bool + * + * @see node_access() + */ +function _node_resource_access($op = 'view', $args = array()) { + // Adds backwards compatability with regression fixed in #1083242 + if (isset($args[0])) { + $args[0] = _services_access_value($args[0], 'node'); + } + + // Make sure we have an object or this all fails, some servers can + // mess up the types. + if (is_array($args[0])) { + $args[0] = (object) $args[0]; + } + elseif (!is_array($args[0]) && !is_object($args[0])) { //This is to determine if it is just a string happens on node/%NID + $args[0] = (object)array('nid' => $args[0]); + } + + if ($op != 'create' && !empty($args)) { + $node = node_load($args[0]->nid); + } + elseif ($op == 'create') { + if (isset($args[0]->type)) { + $node = $args[0]->type; + return node_access($op, $node); + } + else { + return services_error(t('Node type is required'), 406); + } + } + if (isset($node->nid) && $node->nid) { + return node_access($op, $node); + } + else { + return services_error(t('Node @nid could not be found', array('@nid' => $args[0]->nid)), 404); + } +} + +/** + * Generates an array of base64 encoded files attached to a node + * + * @param $nid + * Number. Node ID + * @param $include_file_contents + * Bool Whether or not to include the base64_encoded version of the file. + * @param $get_image_style + * Bool Whether or not to provide image style paths. + * @return + * Array. A list of all files from the given node + */ +function _node_resource_load_node_files($nid, $include_file_contents, $get_image_style) { + module_load_include('inc', 'services', 'resources/file_resource'); + $node = node_load($nid); + + // Hopefully theres another way to get a nodes fields that are a file, but this was the only way I could do it. + $fields = field_info_fields(); + $files = array(); + + // Loop through all of the fields on the site + foreach ($fields as $key => $field) { + //if we are a field type of file + if ($field['type'] == 'image' || $field['type'] == 'file') { + // If this field exists on our current node.. + if (isset($node->{$field['field_name']})) { + // If there are items in the field... + if (isset($node->{$field['field_name']}[LANGUAGE_NONE])) { + // Grab the items given and attach them to the array. + $node_file_field_items = $node->{$field['field_name']}[LANGUAGE_NONE]; + foreach ($node_file_field_items as $file) { + $files[] = _file_resource_retrieve($file['fid'], $include_file_contents, $get_image_style); + } + } + } + } + } + + return $files; +} + +/** + * Returns the comments of a specified node. + * + * @param $nid + * Unique identifier for the node. + * @param $count + * Number of comments to return. + * @param $start + * Which comment to start with. If present, $start and $count are used together + * to create a LIMIT clause for selecting comments. This could be used to do paging. + * @return + * An array of comment objects. + */ +function _node_resource_load_node_comments($nid, $count = 0, $start = 0) { + $query = db_select('comment', 'c'); + $query->innerJoin('node', 'n', 'n.nid = c.nid'); + $query->addTag('node_access'); + $query->fields('c', array('cid')) + ->condition('c.nid', $nid); + + if ($count) { + $query->range($start, $count); + } + + $result = $query->execute() + ->fetchAll(); + + foreach ($result as $record) { + $cids[] = $record->cid; + } + + return !empty($cids) ? comment_load_multiple($cids) : array(); +} + +/** + * Attaches or overwrites file(s) to an existing node. + * + * Example form element used to post files to attach_file: + *
+ * + * + * + * + * + * The name="files[anything]" format is required to use file_save_upload(). + * + * @param $nid + * Node ID of the node the file(s) is being attached to. + * @param $field_name + * Machine name of the field that is attached to the node. + * @param $attach + * Optional. Defaults to true. This means that files will be attached to the + * node, alongside existing files. If the maximum number of files have already + * been uploaded to this node an error is given. + * If false, it removes the files, and attaches the new files uploaded. + * @return + * An array of files that were attached in the form: + * array( + * array( + * fid => N, + * uri => http://site.com/endpoint/file/N + * ), + * ... + * ) + * + * @see file_save_upload() + * @see file + */ +function _node_resource_attach_file($nid, $field_name, $attach, $field_values) { + $node = node_load($nid); + $node_type=$node->type; + + if (empty($node->{$field_name}[LANGUAGE_NONE] )) { + $node->{$field_name}[LANGUAGE_NONE] = array(); + } + + // Validate whether field instance exists and this node type can be edited. + _node_resource_validate_node_type_field_name('update', array($node_type, $field_name)); + + $counter = 0; + if ($attach) { + $counter = count($node->{$field_name}[LANGUAGE_NONE]); + } + else { + $node->{$field_name}[LANGUAGE_NONE] = array(); + } + + $options = array('attach' => $attach, 'file_count' => $counter); + + list($files, $file_objs) = _node_resource_file_save_upload($node_type, $field_name, $options); + // Retrieve the field settings. + $field = field_info_field($field_name); + + foreach ($file_objs as $key => $file_obj) { + if (isset($field_values[$key])) { + foreach ($field_values[$key] as $key => $value) { + $file_obj->$key = $value; + } + } + + $node->{$field_name}[LANGUAGE_NONE][$counter] = (array)$file_obj; + // Check the field display settings. + if (isset($field['settings']['display_field'])) { + // Set the display option. + $node->{$field_name}[LANGUAGE_NONE][$counter]['display'] = $field['settings']['display_field']; + } + $counter++; + } + + node_save($node); + + return $files; +} + +/** + * Services wrapper for file_save_upload. + * + * @see file_save_upload() + * @see file_managed_file_save_upload() + */ +function _node_resource_file_save_upload($node_type, $field_name, $options= array() ) { + // The field_name on node_type should be checked in the access callback. + $instance = field_info_instance('node', $field_name, $node_type); + $field = field_read_field($field_name); + $cardinality = $field['cardinality']; + + // If cardinality is not unlimited check the how many 'slots' we have left. + if (($cardinality > 0) && isset($options['file_count'])) { + // Already uploaded files + $file_already_uploaded_count = $options['file_count']; + // How many files we are going to upload. + $file_upload_count = count($_FILES['files']['name']); + // If we add new files and not replace already uploaded. + if ( (isset($options['attach']) && ($options['attach']) && ($file_already_uploaded_count + $file_upload_count > $cardinality)) + // If we replace existing files. + || ((!isset($options['attach']) || !$options['attach']) && $file_upload_count > $cardinality)) { + return services_error(t('You cannot upload so many files.')); + } + } + + $destination = file_field_widget_uri($field, $instance ); + if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) { + return services_error(t('The upload directory %directory for the file field !name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $destination, '!name' => $field_name))); + } + + $validators = array( + 'file_validate_extensions' => (array)$instance['settings']['file_extensions'], + 'file_validate_size' => array(0 => parse_size($instance['settings']['max_filesize'])), + ); + + $files = $file_objs = array(); + + foreach ($_FILES['files']['name'] as $key => $val) { + + // Let the file module handle the upload and moving. + if (!$file = file_save_upload($key, $validators, $destination, FILE_EXISTS_RENAME) ) { + return services_error(t('Failed to upload file. @upload', array('@upload' => $key)), 406); + } + + if ($file->fid) { + // Add info to the array that will be returned/encdoed to xml/json. + $files[] = array( + 'fid' => $file->fid, + 'uri' => services_resource_uri(array('file', $file->fid)), + ); + $file_objs[] = $file; + } + else { + return services_error(t('An unknown error occurred'), 500); + } + } + + return array($files, $file_objs); +} + +/** + * Helper function to validate data. + * + * @param $op + * Array representing the attributes a node edit form would submit. + * @param $args + * Resource arguments passed through from the original request (node_type, + * field_name). + * + * @return bool + * TRUE/FALSE based on access. + */ +function _node_resource_validate_node_type_field_name($op = 'create', $args = array()) { + $node_type = $args[0]; + $field_name = $args[1]; + + $temp_node= array('type' => $node_type); + + // An invalid node type throws an exception, and stops before the return below. + _node_resource_validate_type($temp_node); + + if (!field_info_instance('node', $field_name, $node_type)) { + return services_error(t('Field name \'@field_name\' not found on node type \'@node_type\'', array('@field_name' => $field_name, '@node_type' => $node_type)), 406); + } + + return TRUE; +} diff --git a/openthess/modules/services/resources/system_resource.inc b/openthess/modules/services/resources/system_resource.inc new file mode 100644 index 0000000..762365f --- /dev/null +++ b/openthess/modules/services/resources/system_resource.inc @@ -0,0 +1,101 @@ + array( + 'actions' => array( + 'connect' => array( + 'access callback' => 'services_access_menu', + 'help' => 'Returns the details of currently logged in user.', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/system_resource'), + 'callback' => '_system_resource_connect', + ), + 'get_variable' => array( + 'help' => 'Returns the value of a system variable using variable_get().', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/system_resource'), + 'callback' => 'variable_get', + 'access arguments' => array('get a system variable'), + 'access arguments append' => FALSE, + 'args' => array( + array( + 'name' => 'name', + 'optional' => FALSE, + 'source' => array('data' => 'name'), + 'description' => t('The name of the variable to return.'), + 'type' => 'string', + ), + array( + 'name' => 'default', + 'optional' => TRUE, + 'source' => array('data' => 'default'), + 'description' => t('The default value to use if this variable has never been set.'), + 'type' => 'string', + ), + ), + ), + 'set_variable' => array( + 'help' => 'Sets the value of a system variable using variable_set().', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/system_resource'), + 'callback' => 'variable_set', + 'access arguments' => array('set a system variable'), + 'access arguments append' => FALSE, + 'args' => array( + array( + 'name' => 'name', + 'optional' => FALSE, + 'source' => array('data' => 'name'), + 'description' => t('The name of the variable to set.'), + 'type' => 'string', + ), + array( + 'name' => 'value', + 'optional' => FALSE, + 'source' => array('data' => 'value'), + 'description' => t('The value to set.'), + 'type' => 'string', + ), + ), + ), + 'del_variable' => array( + 'help' => 'Deletes a system variable using variable_del().', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/system_resource'), + 'callback' => 'variable_del', + 'access arguments' => array('set a system variable'), + 'access arguments append' => FALSE, + 'args' => array( + array( + 'name' => 'name', + 'optional' => FALSE, + 'source' => array('data' => 'name'), + 'description' => t('The name of the variable to delete.'), + 'type' => 'string', + ), + ), + ), + ), + ), + ); +} + +/** + * Returns the details of currently logged in user. + * + * @return + * object with session id, session name and a user object. + */ +function _system_resource_connect() { + global $user; + services_remove_user_data($user); + + $return = new stdClass(); + $return->sessid = session_id(); + $return->session_name = session_name(); + $return->user = $user; + + return $return; +} diff --git a/openthess/modules/services/resources/taxonomy_resource.inc b/openthess/modules/services/resources/taxonomy_resource.inc new file mode 100644 index 0000000..293b7eb --- /dev/null +++ b/openthess/modules/services/resources/taxonomy_resource.inc @@ -0,0 +1,641 @@ + array( + 'operations' => array( + 'retrieve' => array( + 'help' => 'Retrieve a term', + 'callback' => '_taxonomy_term_resource_retrieve', + 'args' => array( + array( + 'name' => 'tid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The tid of the taxonomy term to get', + ), + ), + 'access arguments' => array('access content'), + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'create' => array( + 'help' => 'Create a term', + 'callback' => '_taxonomy_term_resource_create', + 'args' => array( + array( + 'name' => 'term', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The taxonomy term object to create', + 'type' => 'array', + ), + ), + 'access callback' => '_taxonomy_resource_create_access', + 'access arguments append' => TRUE, + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'update' => array( + 'help' => 'Update a term', + 'callback' => '_taxonomy_term_resource_update', + 'args' => array( + array( + 'name' => 'tid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'description' => 'The unique identifier for this taxonomy term.', + 'type' => 'int', + ), + array( + 'name' => 'term', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The taxonomy term data to update', + 'type' => 'array', + ), + ), + 'access callback' => '_taxonomy_resource_update_access', + 'access arguments append' => TRUE, + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'delete' => array( + 'help' => 'Delete the term', + 'callback' => '_taxonomy_term_resource_delete', + 'args' => array( + array( + 'name' => 'tid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + ), + ), + 'access callback' => '_taxonomy_resource_delete_access', + 'access arguments append' => TRUE, + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'index' => array( + 'help' => 'List all terms', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + 'callback' => '_taxonomy_term_resource_index', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_taxonomy_term_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access arguments' => array('access content'), + ), + ), + 'actions' => array( + 'selectNodes' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + 'help' => t('Returns all nodes with provided taxonomy id.'), + 'access arguments' => array('access content'), + 'callback' => 'taxonomy_service_select_nodes', + 'args' => array( + array( + 'name' => 'tid', + 'type' => 'string', + 'description' => t('The vocabulary ids to retrieve, separated by comma.'), + 'source' => array('data' => 'tid'), + 'optional' => FALSE, + ), + array( + 'name' => 'pager', + 'type' => 'int', + 'description' => t('Whether the nodes are to be used with a pager (the case on most Drupal pages) or not (in an XML feed, for example).'), + 'source' => array('data' => 'pager'), + 'optional' => TRUE, + 'default value'=> TRUE, + ), + array( + 'name' => 'limit', + 'type' => 'int', + 'description' => t('Maximum number of nodes to find.'), + 'source' => array('data' => 'limit'), + 'optional' => TRUE, + 'default value'=> FALSE, + ), + array( + 'name' => 'order', + 'type' => 'int', + 'description' => t('The order clause for the query that retrieve the nodes.'), + 'source' => array('data' => 'order'), + 'optional' => TRUE, + 'default value'=> array('t.sticky' => 'DESC', 't.created' => 'DESC'), + ), + ), + ), + ), + ), + 'taxonomy_vocabulary' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + 'operations' => array( + 'retrieve' => array( + 'help' => 'Retrieve a taxonomy vocabulary', + 'callback' => '_taxonomy_vocabulary_resource_retrieve', + 'args' => array( + array( + 'name' => 'vid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The vid of the taxonomy vocabulary to get', + ), + ), + 'access arguments' => array('access content'), + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'create' => array( + 'help' => 'Create a taxonomy vocabulary', + 'callback' => '_taxonomy_vocabulary_resource_create', + 'args' => array( + array( + 'name' => 'vocabulary', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The taxonomy vocabulary object to create', + 'type' => 'array', + ), + ), + 'access arguments' => array('administer taxonomy'), + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'update' => array( + 'help' => 'Update a taxonomy vocabulary', + 'callback' => '_taxonomy_vocabulary_resource_update', + 'args' => array( + array( + 'name' => 'vid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'description' => 'The unique identifier for this taxonomy vocabulary.', + 'type' => 'int', + ), + array( + 'name' => 'vocabulary', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The taxonomy vocabulary data to update', + 'type' => 'array', + ), + ), + 'access arguments' => array('administer taxonomy'), + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'delete' => array( + 'help' => 'Delete a taxonomy vocabulary', + 'callback' => '_taxonomy_vocabulary_resource_delete', + 'args' => array( + array( + 'name' => 'vid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + ), + ), + 'access arguments' => array('administer taxonomy'), + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + ), + 'index' => array( + 'help' => 'List all taxonomy vocabularies', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + 'callback' => '_taxonomy_vocabulary_resource_index', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_taxonomy_vocabulary_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access arguments' => array('access content'), + ), + ), + 'actions' => array( + 'getTree' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/taxonomy_resource'), + 'help' => t('Returns a full list of taxonomy terms.'), + 'access arguments' => array('access content'), + 'callback' => 'taxonomy_service_get_tree', + 'args' => array( + array( + 'name' => 'vid', + 'type' => 'int', + 'description' => t('The vocabulary id to retrieve.'), + 'source' => array('data' => 'vid'), + 'optional' => FALSE, + ), + array( + 'name' => 'parent', + 'type' => 'int', + 'description' => t('The term ID under which to generate the tree. If 0, generate the tree for the entire vocabulary.'), + 'source' => array('data' => 'parent'), + 'default value' => 0, + 'optional' => TRUE, + ), + array( + 'name' => 'maxdepth', + 'type' => 'int', + 'description' => t('The number of levels of the tree to return. Leave NULL to return all levels.'), + 'source' => array('data' => 'maxdepth'), + 'default value' => NULL, + 'optional' => TRUE, + ), + array( + 'name' => 'load_entities', + 'type' => 'int', + 'description' => t('Whether the tree of terms should contain full term entity objects. If 1 (TRUE), a full entity load will occur on the term objects. Otherwise they are partial objects to save execution time and memory consumption. Defaults to 0 (FALSE).'), + 'source' => array('data' => 'load_entities'), + 'default value' => 0, + 'optional' => TRUE, + ), + ), + ), + ), + ), + ); +} + +/** + * Return the results of taxonomy_get_term() for a specified term id. + * + * @param $tid + * Unique identifier for the taxonomy term to retrieve. + * @return + * A term object. + * + * @see taxonomy_get_term() + */ +function _taxonomy_term_resource_retrieve($tid) { + return taxonomy_term_load($tid); +} + +/** + * Create a new taxonomy term based on submitted values. + * + * @param $term + * Array of values for the taxonomy term. + * @return + * Status constant indicating if term was inserted or updated. + * + * @see taxonomy_save_term() + */ +function _taxonomy_term_resource_create($term) { + // Adds backwards compatability with regression fixed in #1083242 + $term = _services_arg_value($term, 'term'); + + $term = (object)$term; + return taxonomy_term_save($term); +} + +/** + * Update a taxonomy term based on submitted values. + * + * @param $tid + * Unique identifier for the taxonomy term to update. + * @param $term + * Array of values for the taxonomy term. + * @return + * Status constant indicating if term was inserted or updated. + * + * @see taxonomy_save_term() + */ +function _taxonomy_term_resource_update($tid, $term) { + // Adds backwards compatability with regression fixed in #1083242 + $term = _services_arg_value($term, 'term'); + + $term = (object) $term; + $term->tid = $tid; + return taxonomy_term_save($term); +} + +/** + * Delete a taxonomy term. + * + * @param $tid + * Unique identifier for the taxonomy term to delete. + * @return + * Status constant indicating deletion. + * + * @see taxonomy_del_term() + */ +function _taxonomy_term_resource_delete($tid) { + return taxonomy_term_delete($tid); +} + + +/** + * Return the results of taxonomy_vocabulary_load() for a specified vocabulary id. + * + * @param $vid + * Unique identifier for the taxonomy term to retrieve. + * @return + * A vocabulary object or FALSE. + * + * @see taxonomy_vocabulary_load() + */ +function _taxonomy_vocabulary_resource_retrieve($vid) { + return taxonomy_vocabulary_load($vid); +} + +/** + * Create a new taxonomy vocabulary based on submitted values. + * + * Here is a sample vocabulary array, taken from + * http://drupaldeveloper.in/article/programmatically-create-vocabulary + * + * @code + * $vocabulary = array( + * 'name' => t("Name"). // Human readable name of the vocabulary + * 'description' => t("Description"), // extended description of the vocabulary + * 'help' => t("help"), // help text + * 'tags' => 0, // 1 to make this vocabulary free tagging + * 'multiple' => 0, // 1 to allow multiple selection + * 'required' => 0, // 1 to make the terms mandatory to be selected + * 'hierarchy' => 0, // 1 to allow and create hierarchy of the terms within the vocabulary + * 'relations' => 0, // 1 to set and allow relation amongst multiple terms + * 'module' => 'mymodule', // provide the module name in which the vocabulary is defined and which is calling this function + * 'node' => array('story' => 1), // content types to which this vocabulary will be attached to + * 'weight' => -9, // set the weight to display the vocabulary in the list + * ); + * @endcode + * + * @param $vocabulary + * Array of values for the taxonomy vocabulary. + * @return + * Status constant indicating if vocabulary was inserted or updated. + * + * @see taxonomy_vocabulary_save() + */ +function _taxonomy_vocabulary_resource_create($vocabulary) { + // Adds backwards compatability with regression fixed in #1083242 + $vocabulary = _services_arg_value($vocabulary, 'vocabulary'); + + $vocabulary = (object) $vocabulary; + return taxonomy_vocabulary_save($vocabulary); +} + +/** + * Update a taxonomy vocabulary based on submitted values. + * + * @param $vid + * Unique identifier for the taxonomy term to retrieve. + * @param $vocabulary + * Array of values for the taxonomy vocabulary. + * @return + * Status constant indicating if vocabulary was inserted or updated. + * + * @see taxonomy_vocabulary_save() + */ +function _taxonomy_vocabulary_resource_update($vid, $vocabulary) { + // Adds backwards compatability with regression fixed in #1083242 + $vocabulary = _services_arg_value($vocabulary, 'vocabulary'); + + $vocabulary = (object) $vocabulary; + $vocabulary->vid = $vid; + return taxonomy_vocabulary_save($vocabulary); +} + +/** + * Delete a taxonomy vocabulary. + * + * @param $vid + * Unique identifier for the taxonomy term to retrieve. + * @return + * Status constant indicating deletion. + * + * @see taxonomy_del_vocabulary() + */ +function _taxonomy_vocabulary_resource_delete($vid) { + return taxonomy_vocabulary_delete($vid); +} + + + +/** + * Services interface to taxonomy_get_tree(). + * + * @see taxonomy_get_tree() + */ +function taxonomy_service_get_tree($vid, $parent = 0, $max_depth = NULL, $load_entities = 0) { + $terms = taxonomy_get_tree($vid, $parent, $max_depth, $load_entities); + if (empty($terms)) { + return services_error(t('No vocabulary with id @vid found.', array('@vid' => $vid)), 404); + } + return $terms; +} + +/** + * Services interface to taxonomy_select_nodes(). + * + * Note that where taxonomy_select_nodes() returns the results + * of a db_query(), this function returns an array of node objects. + * + * @see taxonomy_select_nodes() + * @return + * An array of node objects. + */ +function taxonomy_service_select_nodes($tid = '', $pager, $limit, $order) { + $result = taxonomy_select_nodes($tid, (bool)$pager, $limit, $order); + foreach ($result as $nid) { + $node = node_load($nid); + if ($uri = services_resource_uri(array('node', $nid))) { + $node->uri = $uri; + } + $nodes[] = $node; + } + if (empty($nodes)) { + return services_error(t('No nodes were found with tid @tid', array('@tid' => $tid)), 404); + } + return $nodes; +} + +/** + * Access callback for term updating. + * + * @param $term + * An taxonomy term object + * @return + * Boolean whether or not the user has access to create or edit the term. + */ +function _taxonomy_resource_update_access($tid, $term) { + // Adds backwards compatability with regression fixed in #1083242 + $term = _services_arg_value($term, 'term'); + + $term = (object) $term; + if (!isset($term->vid)) { + return services_error(t('Term object needs vid property.'), 406); + } + return user_access('edit terms in ' . $term->vid) || user_access('administer taxonomy'); +} + +/** + * Access callback for term creating. + * + * @param $term + * An taxonomy term object + * @return + * Boolean whether or not the user has access to create or edit the term. + */ +function _taxonomy_resource_create_access($term) { + // Adds backwards compatability with regression fixed in #1083242 + $term = _services_arg_value($term, 'term'); + + $term = (object) $term; + return user_access('edit terms in ' . $term->vid) || user_access('administer taxonomy'); +} + +/** + * Access callback for term deleting. + * + * @param $term + * An taxonomy term object + * @return + * Boolean whether or not the user has access to delete the term. + */ +function _taxonomy_resource_delete_access($tid) { + $term = taxonomy_term_load($tid); + if (!$term) { + return services_error(t('There is no term with id @tid.', array('@tid' => $tid)), 406); + } + return user_access('delete terms in ' . $term->vid) || user_access('administer taxonomy'); +} + +/** + * Return an array of optionally paged tids baed on a set of criteria. + * + * An example request might look like + * + * http://domain/endpoint/taxonomy_term?fields=tid,name¶meters[tid]=7¶meters[vid]=1 + * + * This would return an array of objects with only tid and name defined, where + * tid = 7 and vid = 1. + * + * @param $page + * Page number of results to return (in pages of 20). + * @param $fields + * The fields you want returned. + * @param $parameters + * An array containing fields and values used to build a sql WHERE clause + * indicating items to retrieve. + * @param $page_size + * Integer number of items to be returned. + * @return + * An array of term objects. + * + * @see _node_resource_index() for more notes + **/ +function _taxonomy_term_resource_index($page, $fields, $parameters, $page_size) { + $taxonomy_select = db_select('taxonomy_term_data', 't') + ->orderBy('vid', 'DESC') + ->orderBy('weight', 'DESC') + ->orderBy('name', 'DESC'); + + services_resource_build_index_query($taxonomy_select, $page, $fields, $parameters, $page_size, 'taxonomy_term'); + + $results = services_resource_execute_index_query($taxonomy_select); + + return services_resource_build_index_list($results, 'taxonomy_term', 'tid'); +} + +/** + * Return an array of optionally paged vids baed on a set of criteria. + * + * An example request might look like + * + * http://domain/endpoint/taxonomy_vocabulary?fields=vid,name¶meters[vid]=2 + * + * This would return an array of objects with only vid and name defined, where + * vid = 2. + * + * @param $page + * Page number of results to return (in pages of 20). + * @param $fields + * The fields you want returned. + * @param $parameters + * An array containing fields and values used to build a sql WHERE clause + * indicating items to retrieve. + * @param $page_size + * Integer number of items to be returned. + * @return + * An array of vocabulary objects. + * + * @todo + * Support node types as parameters. + * + * @see _node_resource_index() for more notes + **/ +function _taxonomy_vocabulary_resource_index($page, $fields, $parameters, $page_size) { + $taxonomy_select = db_select('taxonomy_vocabulary', 't') + ->orderBy('weight', 'DESC') + ->orderBy('name', 'DESC'); + + services_resource_build_index_query($taxonomy_select, $page, $fields, $parameters, $page_size, 'taxonomy_vocabulary'); + + $results = $taxonomy_select->execute(); + + return services_resource_build_index_list($results, 'taxonomy_vocabulary', 'vid'); +} \ No newline at end of file diff --git a/openthess/modules/services/resources/user_resource.inc b/openthess/modules/services/resources/user_resource.inc new file mode 100644 index 0000000..10a4d05 --- /dev/null +++ b/openthess/modules/services/resources/user_resource.inc @@ -0,0 +1,785 @@ + array( + 'operations' => array( + 'retrieve' => array( + 'help' => 'Retrieve a user', + 'callback' => '_user_resource_retrieve', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + 'access callback' => '_user_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'uid', + 'type' => 'int', + 'description' => 'The uid of the user to retrieve.', + 'source' => array('path' => 0), + 'optional' => FALSE, + ), + ), + ), + + 'create' => array( + 'help' => 'Create a user', + 'callback' => '_user_resource_create', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + 'access callback' => '_user_resource_access', + 'access arguments' => array('create'), + 'access arguments append' => FALSE, + 'args' => array( + array( + 'name' => 'account', + 'type' => 'array', + 'description' => 'The user object', + 'source' => 'data', + 'optional' => FALSE, + ), + ), + ), + + 'update' => array( + 'help' => 'Update a user', + 'callback' => '_user_resource_update', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + 'access callback' => '_user_resource_access', + 'access arguments' => array('update'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'uid', + 'type' => 'int', + 'description' => 'Unique identifier for this user', + 'source' => array('path' => 0), + 'optional' => FALSE, + ), + array( + 'name' => 'data', + 'type' => 'array', + 'description' => 'The user object with updated information', + 'source' => 'data', + 'optional' => FALSE, + ), + ), + ), + + 'delete' => array( + 'help' => 'Delete a user', + 'callback' => '_user_resource_delete', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + 'access callback' => '_user_resource_access', + 'access arguments' => array('delete'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'uid', + 'type' => 'int', + 'description' => 'The id of the user to delete', + 'source' => array('path' => 0), + 'optional' => FALSE, + ), + ), + ), + 'index' => array( + 'help' => 'List all users', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + 'callback' => '_user_resource_index', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_user_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access arguments' => array('access user profiles'), + 'access arguments append' => FALSE, + ), + ), + 'actions' => array( + 'login' => array( + 'help' => 'Login a user for a new session', + 'callback' => '_user_resource_login', + 'args' => array( + array( + 'name' => 'username', + 'type' => 'string', + 'description' => 'A valid username', + 'source' => array('data' => 'username'), + 'optional' => FALSE, + ), + array( + 'name' => 'password', + 'type' => 'string', + 'description' => 'A valid password', + 'source' => array('data' => 'password'), + 'optional' => FALSE, + ), + ), + 'access callback' => 'services_access_menu', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + ), + + 'logout' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + 'help' => 'Logout a user session', + 'callback' => '_user_resource_logout', + 'access callback' => 'services_access_menu', + ), + 'token' => array( + 'file' => array( + 'type' => 'inc', + 'module' => 'services', + 'name' => 'user_resource', + ), + 'callback' => '_user_resource_get_token', + 'access callback' => 'services_access_menu', + 'help' => t('Returns the CSRF token.'), + ), + ), + 'targeted_actions' => array( + 'cancel' => array( + 'help' => 'Cancel a user', + 'callback' => '_user_resource_cancel', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource'), + 'access callback' => '_user_resource_access', + 'access arguments' => array('cancel'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'uid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The id of the user to cancel.', + ), + ), + ), + 'password_reset' => array( + 'access callback' => '_user_resource_access', + 'access arguments' => array('password_reset'), + 'access arguments append' => TRUE, + 'callback' => '_user_resource_password_reset', + 'args' => array( + array( + 'name' => 'uid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The id of the user whose password to reset.', + ), + ), + ), + 'resend_welcome_email' => array( + 'access callback' => '_user_resource_access', + 'access arguments' => array('resend_welcome_email'), + 'access arguments append' => TRUE, + 'callback' => '_user_resource_resend_welcome_email', + 'args' => array( + array( + 'name' => 'uid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The id of the user whose welcome email to resend.', + ), + ), + ), + ), + ), + ); + + $definition['user']['actions']['register'] = array_merge($definition['user']['operations']['create'], array( + 'help' => 'Register a user', + )); + + return $definition; +} + +/** + * Get user details. + * + * @param $uid + * UID of the user to be loaded. + * + * @return + * A user object. + * + * @see user_load() + */ +function _user_resource_retrieve($uid) { + $account = user_load($uid); + if (empty($account)) { + return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); + } + + services_remove_user_data($account); + + // Everything went right. + return $account; +} + +/** + * Create a new user. + * + * This function uses drupal_form_submit() and as such expects all input to match + * the submitting form in question. + * + * @param $account + * A object containing account information. The $account object should + * contain, at minimum, the following properties: + * - name (user name) + * - mail (email address) + * - pass (plain text unencrypted password) + * + * These properties can be passed but are optional + * - status (0 for blocked, otherwise will be active by default) + * - notify (1 to notify user of new account, will not notify by default) + * + * Roles can be passed in a roles property which is an associative + * array formatted with '' => '', not including + * the authenticated user role, which is given by default. + * + * @return + * The user object of the newly created user. + */ +function _user_resource_create($account) { + // Adds backwards compatability with regression fixed in #1083242 + $account = _services_arg_value($account, 'account'); + + // Load the required includes for saving profile information + // with drupal_form_submit(). + module_load_include('inc', 'user', 'user.pages'); + + // register a new user + $form_state['values'] = $account; + // Password may be not available as this callback is used for register as well. + $form_state['values']['pass'] = array( + 'pass1' => isset($account['pass']) ?: '', + 'pass2' => isset($account['pass']) ?: '', + ); + $form_state['values']['op'] = variable_get('services_user_create_button_resource_create', t('Create new account')); + + // execute the register form + $form_state['programmed_bypass_access_check'] = FALSE; + drupal_form_submit('user_register_form', $form_state); + // find and store the new user into the form_state + if(isset($form_state['values']['uid'])) { + $form_state['user'] = user_load($form_state['values']['uid']); + } + + // Error if needed. + if ($errors = form_get_errors()) { + return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); + } + else { + $user = array('uid' => $form_state['user']->uid); + if ($uri = services_resource_uri(array('user', $user['uid']))) { + $user['uri'] = $uri; + } + return $user; + } +} + +/** + * Update an existing user. + * + * This function uses drupal_form_submit() and as such expects all input to match + * the submitting form in question. + * + * @param $uid + * Unique identifier for this user + * @param $account + * Fields to modify for this user. + * + * @return + * The modified user object. + */ +function _user_resource_update($uid, $account) { + // Adds backwards compatability with regression fixed in #1083242 + $account = _services_arg_value($account, 'data'); + + $account['uid'] = $uid; + + $account_loaded = user_load($uid); + + // Load the required includes for saving profile information + // with drupal_form_submit(). + module_load_include('inc', 'user', 'user.pages'); + + // If a profile category was passed in, use it. Otherwise default + // to 'account' (for saving core user data.) + $category = 'account'; + if (isset($account['category'])) { + $category = $account['category']; + unset($account['category']); + } + + // Drop any passed in values into the $account var. Anything + // unused by the form just gets ignored. We handle roles and + // password separately. + foreach ($account as $key => $value) { + if ($key != 'pass' && $key != 'roles') { + $form_state['values'][$key] = $value; + } + } + + // Prepare values of roles. Check user's permission before allowing changes to roles. + if (!isset($account['roles']) || !user_access('administer users')) { + $account['roles'] = $account_loaded->roles; + } + foreach ($account['roles'] as $key => $value) { + if (!empty($value)) { + $form_state['values']['roles'][$key] = $key; + } + } + unset($form_state['values']['roles'][2]); + + // Prepare values for password. + if (isset($account['pass'])) { + $form_state['values']['pass']['pass1'] = $account['pass']; + $form_state['values']['pass']['pass2'] = $account['pass']; + } + + // If user is changing name, make sure they have permission. + if (isset($account['name']) && $account['name'] != $account_loaded->name && !(user_access('change own username') || user_access('administer users'))) { + return services_error(t('You are not allowed to change your username.'), 406); + } + + $form_state['values']['op'] = variable_get('services_user_save_button_resource_update', t('Save')); + $form_state['values']['#user_category'] = $category; + $form_state['values']['#account'] = $account_loaded; + $form_state['programmed_bypass_access_check'] = FALSE; + $ret = drupal_form_submit('user_profile_form', $form_state, $account_loaded, $category); + + // Error if needed. + if ($errors = form_get_errors()) { + return services_error(implode(" ", $errors), 406, array('form_errors' => $errors)); + } + else { + services_remove_user_data($account); + return $account; + } +} + +/** + * Delete a user. + * + * @param $uid + * UID of the user to be deleted. + * + * @see user_delete() + */ +function _user_resource_delete($uid) { + if ($uid == 1) { + return services_error(t('The admin user cannot be deleted.'), 403); + } + + $account = user_load($uid); + if (empty($account)) { + return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); + } + user_delete($uid); + + // Everything went right. + return TRUE; +} + +/** + * Cancel a user. + * + * @param $uid + * UID of the user to be canceled. + * + * @see user_cancel() + */ +function _user_resource_cancel($uid) { + if ($uid == 1) { + return services_error(t('The admin user cannot be canceled.'), 403); + } + $account = user_load($uid); + if (empty($account)) { + return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); + } + $edit = array( + 'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE), + ); + // This defult setting is defined under "admin/config/people/accounts". + $default_method = variable_get('user_cancel_method', 'user_cancel_block'); + + switch ($default_method) { + case 'user_cancel_block_unpublish': + // Unpublish nodes (current revisions). + module_load_include('inc', 'node', 'node.admin'); + $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol(); + node_mass_update($nodes, array('status' => 0)); + break; + + case 'user_cancel_reassign': + // Anonymize nodes (current revisions). + module_load_include('inc', 'node', 'node.admin'); + $nodes = db_select('node', 'n')->fields('n', array('nid'))->condition('uid', $account->uid)->execute()->fetchCol(); + node_mass_update($nodes, array('uid' => 0)); + // Anonymize old revisions. + db_update('node_revision')->fields(array('uid' => 0))->condition('uid', $account->uid)->execute(); + // Clean history. + db_delete('history')->condition('uid', $account->uid)->execute(); + break; + } + + _user_cancel($edit, $account, $default_method); + + // Everything went right. + return TRUE; +} + +/** + * Login a user using the specified credentials. + * + * Note this will transfer a plaintext password. + * + * @param $username + * Username to be logged in. + * @param $password + * Password, must be plain text and not hashed. + * + * @return + * A valid session object. + */ +function _user_resource_login($username, $password) { + global $user; + + if ($user->uid) { + // user is already logged in + return services_error(t('Already logged in as @user.', array('@user' => $user->name)), 406); + } + + // Check if account is active. + if (user_is_blocked($username)) { + return services_error(t('The username %name has not been activated or is blocked.', array('%name' => $username)), 403); + } + + $uid = user_authenticate($username, $password); + + // Emulate drupal native flood control + if (empty($uid) && variable_get('services_flood_control_enabled', TRUE)) { + _user_resource_flood_control($username); + } + + if ($uid) { + $user = user_load($uid); + if ($user->uid) { + user_login_finalize(); + + $return = new stdClass(); + $return->sessid = session_id(); + $return->session_name = session_name(); + $return->token = drupal_get_token('services'); + + $account = clone $user; + + services_remove_user_data($account); + + $return->user = $account; + + return $return; + } + } + watchdog('user', 'Invalid login attempt for %username.', array('%username' => $username)); + return services_error(t('Wrong username or password.'), 401); +} + +/** + * Logout the current user. + */ +function _user_resource_logout() { + global $user; + + if (!$user->uid) { + // User is not logged in + return services_error(t('User is not logged in.'), 406); + } + + watchdog('user', 'Session closed for %name.', array('%name' => $user->name)); + + // Destroy the current session. + module_invoke_all('user_logout', $user); + session_destroy(); + + // Load the anonymous user. + $user = drupal_anonymous_user(); + + return TRUE; +} + +/** + * Update the current user logout callback to the new callback with a better return value. + */ +function _user_resource_logout_update_1_1() { + $new_set = array( + 'callback' => '_user_resource_logout_1_1', + ); + return $new_set; +} + +/** + * Logs out the currently logged in user and returns the new user object. + */ +function _user_resource_logout_1_1() { + global $user; + + if (!$user->uid) { + // User is not logged in + return services_error(t('User is not logged in.'), 406); + } + + watchdog('user', 'Session closed for %name.', array('%name' => $user->name)); + + // Destroy the current session. + module_invoke_all('user_logout', $user); + session_destroy(); + + // Load the anonymous user. + $user = drupal_anonymous_user(); + + return $user; +} + +/** + * Send a password reset email for the specified user. + */ +function _user_resource_password_reset($uid) { + global $language; + + $account = user_load($uid); + if (empty($account)) { + return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); + } + + // Mail one time login URL and instructions using current language. + $mail = _user_mail_notify('password_reset', $account, $language); + if (!empty($mail)) { + watchdog('user', 'Password reset instructions mailed to %name at %email.', array('%name' => $account->name, '%email' => $account->mail)); + } + else { + watchdog('user', 'There was an error re-sending password reset instructions mailed to %name at %email', array('%name' => $account->name, '%email' => $account->mail)); + } + // Everything went right. + return TRUE; +} + +/** + * Send a welcome email for the specified user. + */ +function _user_resource_resend_welcome_email($uid) { + global $language; + + $account = user_load($uid); + if (empty($account)) { + return services_error(t('There is no user with ID @uid.', array('@uid' => $uid)), 404); + } + + $user_register = variable_get('user_register', 2); + switch ($user_register) { + case USER_REGISTER_ADMINISTRATORS_ONLY: + $op = 'register_admin_created'; + break; + case USER_REGISTER_VISITORS: + $op = 'register_no_approval_required'; + break; + case USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL: + $op = 'register_pending_approval'; + } + + // Mail the welcome emaiil using current language. + $mail = _user_mail_notify($op, $account, $language); + if (!empty($mail)) { + watchdog('user', 'Welcome message has been re-sent to %name at %email.', array('%name' => $account->name, '%email' => $account->mail)); + } + else { + watchdog('user', 'There was an error re-sending welcome message to %name at %email', array('%name' => $account->name, '%email' => $account->mail)); + } + // Everything went right. + return TRUE; +} + +/** + * Return an array of optionally paged uids baed on a set of criteria. + * + * An example request might look like + * + * http://domain/endpoint/user?fields=uid,name,mail¶meters[uid]=1 + * + * This would return an array of objects with only uid, name and mail defined, + * where uid = 1. + * + * @param $page + * Page number of results to return (in pages of 20). + * @param $fields + * The fields you want returned. + * @param $parameters + * An array containing fields and values used to build a sql WHERE clause + * indicating items to retrieve. + * @param $page_size + * Integer number of items to be returned. + * @return + * An array of user objects. + * + * @see _node_resource_index() for more notes + */ +function _user_resource_index($page, $fields, $parameters, $page_size) { + $user_select = db_select('users', 't') + ->orderBy('created', 'DESC'); + + services_resource_build_index_query($user_select, $page, $fields, $parameters, $page_size, 'user'); + + $results = services_resource_execute_index_query($user_select); + + return services_resource_build_index_list($results, 'user', 'uid'); +} + +/** + * Access check callback for user resource. + */ +function _user_resource_access($op = 'view', $args = array()) { + // Adds backwards compatability with regression fixed in #1083242 + if (isset($args[0])) { + $args[0] = _services_access_value($args[0], array('account', 'data')); + } + + // Check if the user exists if appropriate. + if ($op != 'create' && $op != 'register' ) { + $account = user_load($args[0]); + if (!$account) { + return services_error(t('There is no user with ID @uid.', array('@uid' => $args[0])), 406); + } + } + + global $user; + switch ($op) { + case 'view': + return user_view_access($account); + case 'update': + return ($user->uid == $account->uid || user_access('administer users')); + case 'create': + case 'register': + if (!$user->uid && variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) != USER_REGISTER_ADMINISTRATORS_ONLY) { + return TRUE; + } + else { + return user_access('administer users'); + } + case 'password_reset': + return TRUE; + case 'delete': + case 'cancel': + case 'resend_welcome_email': + return user_access('administer users'); + } +} + +/** + * Changes the user/login endpoint to accept the same parameters as the user/register endpoint, namely + * "name" instead of "username" and "pass" instead of "password" + */ +function _user_resource_login_update_1_1() { + $new_set = array( + 'args' => array( + array( + 'name' => 'name', + 'type' => 'string', + 'description' => 'A valid username', + 'source' => array('data' => 'name'), + 'optional' => FALSE, + ), + array( + 'name' => 'pass', + 'type' => 'string', + 'description' => 'A valid password', + 'source' => array('data' => 'pass'), + 'optional' => FALSE, + ), + ), + ); + return $new_set; +} + +function _user_resource_get_token() { + return array('token' => drupal_get_token('services')); +} + + + +/** + * Callback that emulate drupal native flood control. + * @param $username + * Username we register flood event for + * @see user_login_authenticate_validate() + * @see user_login_final_validate() + */ +function _user_resource_flood_control($username) { + $flood_message = ''; + + // Always register an IP-based failed login event. + flood_register_event('failed_login_attempt_ip', variable_get('user_failed_login_ip_window', 3600)); + + if (!flood_is_allowed('failed_login_attempt_ip', variable_get('user_failed_login_ip_limit', 50), variable_get('user_failed_login_ip_window', 3600))) { + $flood_message = t('This IP address is temporarily blocked.'); + } + + $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $username))->fetchObject(); + + if ($account) { + if (variable_get('user_failed_login_identifier_uid_only', FALSE)) { + // Register flood events based on the uid only, so they apply for any + // IP address. This is the most secure option. + $identifier = $account->uid; + } + else { + // The default identifier is a combination of uid and IP address. This + // is less secure but more resistant to denial-of-service attacks that + // could lock out all users with public user names. + $identifier = $account->uid . '-' . ip_address(); + flood_register_event('failed_login_attempt_user', variable_get('user_failed_login_user_window', 21600), $identifier); + } + + // Don't allow login if the limit for this user has been reached. + // Default is to allow 5 failed attempts every 6 hours. + if (!flood_is_allowed('failed_login_attempt_user', variable_get('user_failed_login_user_limit', 5), variable_get('user_failed_login_user_window', 21600), $identifier)) { + $flood_message = t('Account is temporarily blocked.'); + } + } + + // Throw exception if have blocking + if (!empty($flood_message)) { + services_error($flood_message, 406); + } +} \ No newline at end of file diff --git a/openthess/modules/services/servers/rest_server/README.markdown b/openthess/modules/services/servers/rest_server/README.markdown new file mode 100644 index 0000000..40d789e --- /dev/null +++ b/openthess/modules/services/servers/rest_server/README.markdown @@ -0,0 +1,78 @@ +README +=================== + +This is a brief introduction to how the rest server works. See the [services_oop][services_oop] module to find out more about how you easily can expose functionality in a resource-oriented way. + +All this depends on the functionality added by the [oauth-rest branch of services][oauth-rest]. + +Controllers +------------------- + +Tabulation of the controller mapping for the REST server. Requests gets mapped to different controllers based on the HTTP method used and the number of parts in the path. + +Count refers to the number of path parts that comes after the path that identifies the resource type. The request for `/services/rest/node/123` would have the count 1, as `/services/rest/node` identifies the resource. + + X = CRUD + A = Action + T = Targeted action + R = Relationship request + + COUNT |0|1|2|3|4|N| + ------------------- + GET |X|X|R|R|R|R| + ------------------- + POST |X|A|T|T|T|T| + ------------------- + PUT | |X| | | | | + ------------------- + DELETE| |X| | | | | + ------------------- + +CRUD +------------------- + +The basis of the REST server. + + Create: POST /services/rest/node + body data + Retrieve: GET /services/rest/node/123 + Update: PUT /services/rest/node/123 + body data + Delete: DELETE /services/rest/node/123 + +And last but least, the little bastard sibling to Retrieve that didn't get it's place in the acronym: + + Index: GET /services/rest/node + +In the REST server the index often doubles as a search function. The comment resource allows queries like the following for checking for new comments on a node (where 123456 is the timestamp for the last check and 123600 is now): + + New comments: GET /services/comment?nid=123×tamp=123456: + Comments in the last hour: GET /services/comment?timestamp=120000:123600 + +Actions +------------------- + +Actions are performed directly on the resource type, not a individual resource. The following example is hypothetical (but plausible). Say that you want to expose a API for the [apachesolr][apachesolr] module. One of the things that could be exposed is the functionality to reindex the whole site. + + Publish: POST /services/rest/apachesolr/reindex + +Targeted actions +------------------- + +Targeted actions acts on a individual resource. A good, but again - hypothetical, example would be the publishing and unpublishing of nodes. + + Publish: POST /services/rest/node/123/publish + +Relationships +------------------- + +Relationship requests are convenience methods (sugar) to get something thats related to a individual resource. A real example would be the relationship that the [comment_resource][comment_resource] module adds to the node resource: + + Get comments: GET /services/rest/node/123/comments + +This more or less duplicates the functionality of the comment index: + + Get comments: GET /services/rest/comments?nid=123 + +[apachesolr]: http://drupal.org/project/apachesolr "Apache Solr Search Integration" +[comment_resource]: http://github.com/hugowetterberg/comment_resource "Comment resource" +[services_oop]: http://github.com/hugowetterberg/services_oop "Services OOP" +[oauth-rest]: http://github.com/hugowetterberg/services/tree/oauth-rest "Services oAuth REST branch" \ No newline at end of file diff --git a/openthess/modules/services/servers/rest_server/includes/RESTServer.inc b/openthess/modules/services/servers/rest_server/includes/RESTServer.inc new file mode 100755 index 0000000..4a612b8 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/includes/RESTServer.inc @@ -0,0 +1,689 @@ +context = $context; + $this->negotiator = $negotiator; + + $this->resources = $resources; + $this->parsers = $parsers; + $this->formatters = $formatters; + } + + /** + * Handles the call to the REST server + */ + public function handle() { + $controller = $this->getController(); + + $formatter = $this->getResponseFormatter(); + + services_set_server_info('resource_uri_formatter', array(&$this, 'uri_formatter')); + + try { + // Parse the request data + $arguments = $this->getControllerArguments($controller); + $result = services_controller_execute($controller, $arguments); + } + catch (ServicesException $e) { + $result = $this->handleException($e); + } + + return $this->render($formatter, $result); + } + + /** + * Controller is part of the resource like + * + * array( + * 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + * 'callback' => '_node_resource_create', + * 'args' => array( + * array( + * 'name' => 'node', + * 'optional' => FALSE, + * 'source' => 'data', + * 'description' => 'The node data to create', + * 'type' => 'array', + * ), + * ), + * 'access callback' => '_node_resource_access', + * 'access arguments' => array('create'), + * 'access arguments append' => TRUE, + * ), + * + * This method determines what is the controller responsible for processing of the request. + * + * @return array + */ + protected function getController() { + if (empty($this->controller)) { + $resource_name = $this->getResourceName(); + + if (empty($resource_name) || !isset($this->resources[$resource_name])) { + return services_error(t('Could not find resource @name.', array('@name' => $resource_name)), 404); + } + + $resource = $this->resources[$resource_name]; + $this->controller = $this->resolveControllerApplyVersion($resource, $resource_name); + + if (empty($this->controller)) { + return services_error(t('Could not find the controller.'), 404); + } + } + + return $this->controller; + } + + /** + * Wrapper around resolveController() to apply version. + * + * @param array $resource + * Resource definition + * @param string $resource_name + * Name of the resource. Needed for applying version. + * + * @return array $controller + * Controller definition + */ + protected function resolveControllerApplyVersion($resource, $resource_name) { + $apply_version_method = ''; + $controller = $this->resolveController($resource, $apply_version_method); + services_request_apply_version($controller, array('method' => $apply_version_method, 'resource' => $resource_name)); + + return $controller; + } + + /** + * Canonical path is the url of the request without path of endpoint. + * + * For example endpoint has path 'rest'. Canonical of request to url + * 'rest/node/1.php' will be 'node/1.php'. + * + * @return string + */ + public function getCanonicalPath() { + // Use drupal_static so we can clear this static cache during unit testing. + // @see MockServicesRESTServerFactory constructor. + $canonical_path = &drupal_static('RESTServerGetCanonicalPath'); + if (empty($canonical_path)) { + $canonical_path = $this->context->getCanonicalPath(); + $canonical_path = $this->negotiator->getParsedCanonicalPath($canonical_path); + } + return $canonical_path; + } + + /** + * Explode canonical path to parts by '/'. + * + * @return array + */ + protected function getCanonicalPathArray() { + $canonical_path = $this->getCanonicalPath(); + $canonical_path_array = explode('/', $canonical_path); + + return $canonical_path_array; + } + + /** + * Example. We have endpoint with path 'rest'. + * Request is done to url /rest/node/1.php'. + * Name of resource in this case is 'node'. + * + * @return string + */ + protected function getResourceName() { + $canonical_path_array = $this->getCanonicalPathArray(); + $resource_name = array_shift($canonical_path_array); + + return $resource_name; + } + + /** + * Response formatter is responsible for encoding the response. + * + * @return array + * example: + * array( + * 'xml' => array( + * 'mime types' => array('application/xml', 'text/xml'), + * 'formatter class' => 'ServicesXMLFormatter', + * ), + * ) + */ + protected function getResponseFormatter() { + $mime_type = ''; + + $canonical_path_not_parsed = $this->context->getCanonicalPath(); + $response_format = $this->getResponseFormatFromURL($canonical_path_not_parsed); + + if (empty($response_format)) { + $response_format = $this->getResponseFormatContentTypeNegotiations($mime_type, $canonical_path_not_parsed, $this->formatters); + } + + $formatter = array(); + + if (isset($this->formatters[$response_format])) { + $formatter = $this->formatters[$response_format]; + } + + // Check if we support the response format and determine the mime type + if (empty($mime_type) && !empty($formatter)) { + $mime_type = $formatter['mime types'][0]; + } + + if (empty($response_format) || empty($mime_type)) { + return services_error(t('Unknown or unsupported response format.'), 406); + } + + // Set the content type and render output. + drupal_add_http_header('Content-type', $mime_type); + + return $formatter; + } + + + /** + * Retrieve formatter from URL. If format is in the path, we remove it from $canonical_path. + * + * For example /. + * + * @param $canonical_path + * + * @return string + */ + protected function getResponseFormatFromURL($canonical_path) { + return $this->negotiator->getResponseFormatFromURL($canonical_path); + } + + /** + * Determine response format and mime type using headers to negotiate content types. + * + * @param string $mime_type + * Mime type. This variable to be overriden. + * @param string $canonical_path + * Canonical path of the request. + * @param array $formats + * Enabled formats by endpoint. + * + * @return string + * Negotiated response format. For example 'json'. + */ + protected function getResponseFormatContentTypeNegotiations(&$mime_type, $canonical_path, $formats) { + return $this->negotiator->getResponseFormatContentTypeNegotiations($mime_type, $canonical_path, $formats, $this->context); + } + + /** + * Determine the request method + */ + protected function getRequestMethod() { + return $this->context->getRequestMethod(); + } + + /** + * Formats a resource uri + * + * @param array $path + * An array of strings containing the component parts of the path to the resource. + * @return string + * Returns the formatted resource uri + */ + public function uri_formatter($path) { + return url($this->context->getEndpointPath() . '/' . join($path, '/'), array( + 'absolute' => TRUE, + )); + } + + /** + * Parses controller arguments from request + * + * @param array $controller + * The controller definition + * @return void + */ + protected function getControllerArguments($controller) { + $path_array = $this->getCanonicalPathArray(); + array_shift($path_array); + + $data = $this->parseRequestBody(); + drupal_alter('rest_server_request_parsed', $data, $controller); + + $headers = $this->parseRequestHeaders(); + drupal_alter('rest_server_headers_parsed', $headers); + + $sources = array( + 'path' => $path_array, + 'param' => $this->context->getGetVariable(), + 'data' => $data, + 'headers' => $headers, + ); + // Map source data to arguments. + return $this->getControllerArgumentsFromSources($controller, $sources); + } + + /** + * array $controller + * Controller definition + * array $sources + * Array of sources for arguments. Consists of following elements: + * 'path' - path requested + * 'params' - GET variables + * 'data' - parsed POST data + * 'headers' - request headers + * + * @return array + */ + protected function getControllerArgumentsFromSources($controller, $sources) { + $arguments = array(); + if (!isset($controller['args'])) { + return array(); + } + + foreach ($controller['args'] as $argument_number => $argument_info) { + // Fill in argument from source + if (isset($argument_info['source'])) { + $argument_source = $argument_info['source']; + if (is_array($argument_source)) { + $argument_source_keys = array_keys($argument_source); + $source_name = reset($argument_source_keys); + $argument_name = $argument_source[$source_name]; + // Path arguments can be only integers. i.e.'path' => 0 and not 'path' => '0'. + if ($source_name == 'path') { + $argument_name = (int) $argument_name; + } + if (isset($sources[$source_name][$argument_name])) { + $arguments[$argument_number] = $sources[$source_name][$argument_name]; + } + } + else { + if (isset($sources[$argument_source])) { + $arguments[$argument_number] = $sources[$argument_source]; + } + } + // Convert to specific data type. + if (isset($argument_info['type']) && isset($arguments[$argument_number])) { + switch ($argument_info['type']) { + case 'array': + $arguments[$argument_number] = (array) $arguments[$argument_number]; + break; + } + } + } + // When argument isn't set, insert default value if provided or + // throw a exception if the argument isn't optional. + if (!isset($arguments[$argument_number])) { + if (!isset($argument_info['optional']) || !$argument_info['optional']) { + return services_error(t('Missing required argument @arg', array('@arg' => $argument_info['name'])), 401); + } + // Set default value or NULL if default value is not set. + $arguments[$argument_number] = isset($argument_info['default value']) ? $argument_info['default value'] : NULL; + } + } + return $arguments; + } + + protected function parseRequestHeaders() { + $headers = array(); + $http_if_modified_since = $this->context->getServerVariable('HTTP_IF_MODIFIED_SINCE'); + if (!empty($http_if_modified_since)) { + $headers['IF_MODIFIED_SINCE'] = strtotime(preg_replace('/;.*$/', '', $http_if_modified_since)); + } + return $headers; + } + + /** + * Parse request body based on $_SERVER['CONTENT_TYPE'].s + * + * @return array|mixed + */ + protected function parseRequestBody() { + $method = $this->getRequestMethod(); + switch ($method) { + case 'POST': + case 'PUT': + $server_content_type = $this->context->getServerVariable('CONTENT_TYPE'); + if (!empty($server_content_type)) { + $type = $this->parseContentHeader($server_content_type); + } + + // Get the mime type for the request, default to form-urlencoded + if (isset($type['value'])) { + $mime = $type['value']; + } + else { + $mime = 'application/x-www-form-urlencoded'; + } + + // Get the parser for the mime type + $parser = $this->matchParser($mime, $this->parsers); + if (!$parser) { + return services_error(t('Unsupported request content type @mime', array('@mime' => $mime)), 406); + } + + $data = array(); + if (class_exists($parser) && in_array('ServicesParserInterface', class_implements($parser))) { + $parser_object = new $parser; + $data = $parser_object->parse($this->context); + } + return $data; + + default: + return array(); + } + } + + /** + * Extract value of the header string. + * + * @param string $value + * + * @return array $type + * Value that is used $type['value'] + */ + protected function parseContentHeader($value) { + $ret_val = array(); + $value_pattern = '/^([^;]+)(;\s*(.+)\s*)?$/'; + $param_pattern = '/([a-z]+)=(([^\"][^;]+)|(\"(\\\"|[^"])+\"))/'; + $vm=array(); + + if (preg_match($value_pattern, $value, $vm)) { + $ret_val['value'] = $vm[1]; + if (count($vm)>2) { + $pm = array(); + if (preg_match_all($param_pattern, $vm[3], $pm)) { + $pcount = count($pm[0]); + for ($i=0; $i<$pcount; $i++) { + $value = $pm[2][$i]; + if (drupal_substr($value, 0, 1) == '"') { + $value = stripcslashes(drupal_substr($value, 1, mb_strlen($value)-2)); + } + $ret_val['param'][$pm[1][$i]] = $value; + } + } + } + } + + return $ret_val; + } + + /** + * Render results using formatter. + * + * @param array $formatter + * Formatter definition + * @param $result + * Value to be rendered + * + * @return string + * Rendered result + */ + protected function render($formatter, $result) { + if ( !isset($formatter['formatter class']) + || array_search('ServicesFormatterInterface', class_implements($formatter['formatter class'])) === FALSE) { + return services_error('Formatter is invalid.', 500); + } + $formatter_object = new $formatter['formatter class']; + return $formatter_object->render($result); + } + + /** + * Matches a mime-type against a set of parsers. + * + * @param string $mime + * The mime-type of the request. + * @param array $parsers + * An associative array of parser callbacks keyed by mime-type. + * @return mixed + * Returns a parser callback or FALSE if no match was found. + */ + protected function matchParser($mime, $parsers) { + $mimeparse = $this->negotiator->mimeParse(); + $mime_type = $mimeparse->best_match(array_keys($parsers), $mime); + + return ($mime_type) ? $parsers[$mime_type] : FALSE; + } + + /** + * Determine controller. + * + * @param array $resource + * Full definition of the resource. + * @param string $operation + * Type of operation ('index', 'retrieve' etc.). We are going to override this variable. + * Needed for applying version. + * + * @return array + * Controller definition. + */ + protected function resolveController($resource, &$operation) { + $request_method = $this->getRequestMethod(); + + $canonical_path_array = $this->getCanonicalPathArray(); + array_shift($canonical_path_array); + + $canon_path_count = count($canonical_path_array); + $operation_type = NULL; + $operation = NULL; + + // For any HEAD request return response "200 OK". + if ($request_method == 'HEAD') { + return services_error('OK', 200); + } + + // For any OPTIONS request return only the headers. + if ($request_method == 'OPTIONS') { + exit; + } + + // We do not group "if" conditions on purpose for better readability. + + // 'index' method. + if ( $request_method == 'GET' + && isset($resource['operations']['index']) + && $this->checkNumberOfArguments($canon_path_count, $resource['operations']['index']) + ) { + $operation_type = 'operations'; + $operation = 'index'; + } + + // 'retrieve' method. + // First path element should be not empty. + if ( $request_method == 'GET' + && $canon_path_count >= 1 + && isset($resource['operations']['retrieve']) + && $this->checkNumberOfArguments($canon_path_count, $resource['operations']['retrieve']) + && !empty($canonical_path_array[0]) + ) { + $operation_type = 'operations'; + $operation = 'retrieve'; + } + + // 'relationships' + // First path element should be not empty, + // second should be name of targeted action. + if ( $request_method == 'GET' + && $canon_path_count >= 2 + && isset($resource['relationships'][$canonical_path_array[1]]) + && $this->checkNumberOfArguments($canon_path_count, $resource['relationships'][$canonical_path_array[1]], 1) + && isset($canonical_path_array[0]) + ) { + $operation_type = 'relationships'; + $operation = $canonical_path_array[1]; + } + + // 'update' + // First path element should be not empty. + if ( $request_method == 'PUT' + && $canon_path_count >= 1 + && isset($resource['operations']['update']) + && $this->checkNumberOfArguments($canon_path_count, $resource['operations']['update']) + && !empty($canonical_path_array[0]) + ) { + $operation_type = 'operations'; + $operation = 'update'; + } + + // 'delete' + // First path element should be not empty. + if ( $request_method == 'DELETE' + && $canon_path_count >= 1 + && isset($resource['operations']['delete']) + && $this->checkNumberOfArguments($canon_path_count, $resource['operations']['delete']) + && !empty($canonical_path_array[0]) + ) { + $operation_type = 'operations'; + $operation = 'delete'; + } + + // 'create' method. + // First path element should be not empty. + if ( $request_method == 'POST' + && isset($resource['operations']['create']) + && $this->checkNumberOfArguments($canon_path_count, $resource['operations']['create']) + ) { + $operation_type = 'operations'; + $operation = 'create'; + } + + // 'actions' + // First path element should be action name + if ( $request_method == 'POST' + && $canon_path_count >= 1 + && isset($resource['actions'][$canonical_path_array[0]]) + && $this->checkNumberOfArguments($canon_path_count, $resource['actions'][$canonical_path_array[0]], 1) + ) { + $operation_type = 'actions'; + $operation = $canonical_path_array[0]; + } + + // 'targeted_actions' + // First path element should be not empty, + // second should be name of targeted action. + if ( $request_method == 'POST' + && $canon_path_count >= 2 + && isset($resource['targeted_actions'][$canonical_path_array[1]]) + && $this->checkNumberOfArguments($canon_path_count, $resource['targeted_actions'][$canonical_path_array[1]], 1) + && !empty($canonical_path_array[0]) + ) { + $operation_type = 'targeted_actions'; + $operation = $canonical_path_array[1]; + } + + if (empty($operation_type) || empty($operation) || empty($resource[$operation_type][$operation])) { + return FALSE; + } + + $controller = $resource[$operation_type][$operation]; + + if (isset($resource['endpoint']['operations'][$operation]['settings'])) { + // Add the endpoint's settings for the specified operation. + $controller['endpoint'] = $resource['endpoint']['operations'][$operation]['settings']; + } + + if (isset($resource['file']) && empty($controller['file'])) { + $controller['file'] = $resource['file']; + } + + return $controller; + } + + /** + * Count possible numbers of 'path' arguments of the method. + */ + protected function checkNumberOfArguments($args_number, $resource_operation, $required_args = 0) { + $not_required_args = 0; + + if (isset($resource_operation['args'])) { + foreach ($resource_operation['args'] as $argument) { + if (isset($argument['source']) && is_array($argument['source']) && isset($argument['source']['path'])) { + if (!empty($argument['optional'])) { + $not_required_args++; + } + else { + $required_args++; + } + } + } + } + + return $args_number >= $required_args && $args_number <= $required_args + $not_required_args; + } + + /** + * Set proper header and message in case of exception. + * + * @param object $exception + * Exception object + * @param array $controller + * Controller that was executed. + * @param array $arguments + * Set of arguments. + * + * @return string $error_data + * Error message from exception. + */ + public function handleException($exception, $controller = array(), $arguments = array()){ + $error_code = $exception->getCode(); + $error_message = $exception->getMessage(); + $error_data = method_exists($exception, 'getData') ? $exception->getData() : ''; + + switch ($error_code) { + case 204: + $error_header_status_message = '204 No Content: ' . $error_message; + break; + case 304: + $error_header_status_message = '304 Not Modified: ' . $error_message; + break; + case 401: + $error_header_status_message = '401 Unauthorized: ' . $error_message; + break; + case 404: + $error_header_status_message = '404 Not found: ' . $error_message; + break; + case 406: + $error_header_status_message = '406 Not Acceptable: ' . $error_message; + break; + case 200: + $error_header_status_message = '200 ' . $error_message; + break; + default: + if ($error_code >= 400 && $error_code < 600) { + $error_header_status_message = $error_code . ' :' . $error_message; + } + else { + $error_header_status_message = '500 Internal Server Error: An error occurred (' . $error_code . '): ' . $error_message; + } + break; + } + + $error_alter_array = array( + 'code' => $error_code, + 'header_message' => &$error_header_status_message, + 'body_data' => &$error_data, + ); + drupal_alter('rest_server_execute_errors', $error_alter_array, $controller, $arguments); + + drupal_add_http_header('Status', strip_tags($error_header_status_message)); + + return $error_data; + } +} diff --git a/openthess/modules/services/servers/rest_server/includes/ServicesContentTypeNegotiator.inc b/openthess/modules/services/servers/rest_server/includes/ServicesContentTypeNegotiator.inc new file mode 100644 index 0000000..4b2f1fb --- /dev/null +++ b/openthess/modules/services/servers/rest_server/includes/ServicesContentTypeNegotiator.inc @@ -0,0 +1,111 @@ +/. + * + * @param $canonical_path + * + * @return string + */ + public function getResponseFormatFromURL($canonical_path) { + $matches = $this->getCanonicalPathMatches($canonical_path); + return $matches[2]; + } + + /** + * Parse canonical path. It may have extension in the end (example node/1.json). + * This function returns canonical path without extension. + * + * @param string $canonical_path + * + * @return string + * Canonical path without extension. + */ + public function getParsedCanonicalPath($canonical_path) { + $matches = $this->getCanonicalPathMatches($canonical_path); + return $matches[1]; + } + + /** + * Preg match canonical path to split it to clean path and extension. + * + * @param string $canonical_path + * Canonical path with extension. + * @return array + * Array of matches. + */ + public function getCanonicalPathMatches($canonical_path) { + $matches = array(); + if (preg_match('/^(.+)\.([^\.^\/]+)$/', $canonical_path, $matches)) { + return $matches; + } + return array('', $canonical_path, ''); + } + + /** + * Determine response format and mime type using headers to negotiate content types. + * + * @param string $mime_type + * Mime type. This variable to be overriden. + * @param string $canonical_path + * Canonical path of the request. + * @param array $formats + * Enabled formats by endpoint. + * + * @return string + * Negotiated response format. For example 'json'. + */ + public function getResponseFormatContentTypeNegotiations(&$mime_type, $canonical_path, $formats, $context) { + drupal_add_http_header('Vary', 'Accept'); + + // Negotiate response format based on accept-headers if we + // don't have a response format. + $mime_candidates = array(); + $mime_map = array(); + + foreach ($formats as $format => $formatter) { + foreach ($formatter['mime types'] as $m) { + $mime_candidates[] = $m; + $mime_map[$m] = $format; + } + } + + // Get the best matching format, default to json + $response_format = variable_get('rest_server_default_response_format', 'json'); + $http_accept = $context->getServerVariable('HTTP_ACCEPT'); + if (!empty($http_accept)) { + $mime = $this->mimeParse(); + $mime_type = $mime->best_match($mime_candidates, $http_accept); + $response_format = isset($mime_map[$mime_type]) ? $mime_map[$mime_type] : ''; + } + + return $response_format; + } + + /** + * Create a instance of the Mimeparse utility class. + * + * @return Mimeparse + */ + public function mimeParse() { + static $mimeparse; + if (!$mimeparse) { + module_load_include('php', 'rest_server', 'lib/mimeparse'); + $mimeparse = new Mimeparse(); + } + return $mimeparse; + } +} diff --git a/openthess/modules/services/servers/rest_server/includes/ServicesContext.inc b/openthess/modules/services/servers/rest_server/includes/ServicesContext.inc new file mode 100644 index 0000000..0c37390 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/includes/ServicesContext.inc @@ -0,0 +1,188 @@ +data['endpoint_path'] = $endpoint_path; + } + + /** + * Build all context data from global variables. + */ + public function buildFromGlobals() { + $this->data['get'] = $_GET; + $this->data['server'] = $_SERVER; + $this->data['post'] = $_POST; + $this->data['request_body'] = ''; + if ($this->isRequestHasPostBody()) { + $this->data['request_body'] = $this->getRequestBodyData(); + } + } + + /** + * Retrieve endpoint path. It is saved in constructor. + * + * @return string + */ + public function getEndpointPath() { + return $this->data['endpoint_path']; + } + + /** + * Retrieve canonical path. + * + * @return string + */ + public function getCanonicalPath() { + if (!isset($this->data['canonical_path'])) { + $endpoint_path = $this->getEndpointPath(); + $endpoint_path_len = drupal_strlen($endpoint_path . '/'); + $this->data['canonical_path'] = drupal_substr($this->data['get']['q'], $endpoint_path_len); + } + + return $this->data['canonical_path']; + } + + /** + * Determine Request Method of the request. + * + * @return string + * Name of request method (i.e. GET, POST, PUT ...). + */ + public function getRequestMethod() { + if (!isset($this->data['request_method'])) { + $this->data['request_method'] = $this->getRequestMethodFromGlobals(); + } + + return $this->data['request_method']; + } + + /** + * Retrieve request method from global variables. + * + * @return string + * For example GET, POST + */ + protected function getRequestMethodFromGlobals() { + $server = &$this->data['server']; + $get = &$this->data['get']; + + $method = $server['REQUEST_METHOD']; + if ($method == 'POST' && isset($server['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $method = $server['HTTP_X_HTTP_METHOD_OVERRIDE']; + } + if ($method == 'POST' && isset($get['_method']) && $get['_method']) { + $method = $get['_method']; + } + if (isset($get['_method'])) { + unset($get['_method']); + } + + return $method; + } + + /** + * Determine whether we should expect request body to be available. + * + * @return bool + */ + protected function isRequestHasPostBody() { + $method = $this->getRequestMethod(); + return (in_array($method, array('POST', 'PUT'))); + } + + /** + * Fetch request body using php://input handler. + * + * @return string + * Content of the request body. + */ + protected function getRequestBodyData() { + return file_get_contents('php://input'); + } + + /** + * Return value of global $_POST. + * + * @return string + */ + public function getPostData() { + return $this->data['post']; + } + + /** + * Return value of the request body. + * + * @return string + */ + public function getRequestBody() { + return $this->data['request_body']; + } + + /** + * Access to $_SERVER variables. + * + * @param string $variable_name + * Key of the server variable. + * + * @return string + * Value of the server variable. + */ + public function getServerVariable($variable_name) { + if (isset($this->data['server'][$variable_name])) { + return $this->data['server'][$variable_name]; + } + else { + if ($variable_name == 'CONTENT_TYPE' && isset($this->data['server']['HTTP_CONTENT_TYPE'])) { + return $this->data['server']['HTTP_CONTENT_TYPE']; + } + } + } + + /** + * Access to $_GET variables. + * + * @param string $variable_name + * Name of the variable or NULL if all content of $_GET to be returned. + * + * @return mixed + * Value of variable or array of all variables. + */ + public function getGetVariable($variable_name = NULL) { + if (empty($variable_name)) { + return $this->data['get']; + } + if (isset($this->data['get'][$variable_name])) { + return $this->data['get'][$variable_name]; + } + } +} \ No newline at end of file diff --git a/openthess/modules/services/servers/rest_server/includes/ServicesFormatter.inc b/openthess/modules/services/servers/rest_server/includes/ServicesFormatter.inc new file mode 100644 index 0000000..b23bd9a --- /dev/null +++ b/openthess/modules/services/servers/rest_server/includes/ServicesFormatter.inc @@ -0,0 +1,101 @@ +createElement('result'); + $doc->appendChild($root); + + $this->xml_recursive($doc, $root, $data); + + return $doc->saveXML(); + } + + private function xml_recursive(&$doc, &$parent, $data) { + if (is_object($data)) { + $data = get_object_vars($data); + } + + if (is_array($data)) { + $assoc = FALSE || empty($data); + foreach ($data as $key => $value) { + if (is_numeric($key)) { + $key = 'item'; + } + else { + $assoc = TRUE; + $key = preg_replace('/[^A-Za-z0-9_]/', '_', $key); + $key = preg_replace('/^([0-9]+)/', '_$1', $key); + } + $element = $doc->createElement($key); + $parent->appendChild($element); + $this->xml_recursive($doc, $element, $value); + } + + if (!$assoc) { + $parent->setAttribute('is_array', 'true'); + } + } + elseif ($data !== NULL) { + $parent->appendChild($doc->createTextNode($data)); + } + } +} + +class ServicesYAMLFormatter implements ServicesFormatterInterface { + public function render($data) { + if (($library = libraries_load('spyc')) && !empty($library['loaded'])) { + return Spyc::YAMLDump($data, 4, 60); + } + else { + watchdog('REST Server', 'Spyc library not found!', array(), WATCHDOG_ERROR); + return ''; + } + } +} + +class ServicesBencodeFormatter implements ServicesFormatterInterface { + public function render($data) { + module_load_include('php', 'rest_server', 'lib/bencode'); + return bencode($data); + } +} diff --git a/openthess/modules/services/servers/rest_server/includes/ServicesParser.inc b/openthess/modules/services/servers/rest_server/includes/ServicesParser.inc new file mode 100644 index 0000000..c5d09a7 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/includes/ServicesParser.inc @@ -0,0 +1,142 @@ +getRequestBody(), $data); + return $data; + } +} + +class ServicesParserPHP implements ServicesParserInterface { + public function parse(ServicesContextInterface $context) { + return unserialize($context->getRequestBody()); + } +} + +class ServicesParserXML implements ServicesParserInterface { + public function parse(ServicesContextInterface $context) { + // get/hold the old error state + $old_error_state = libxml_use_internal_errors(1); + + // clear all libxml errors + libxml_clear_errors(); + + // get a now SimpleXmlElement object from the XML string + $xml_data = simplexml_load_string($context->getRequestBody()); + + // if $xml_data is Null then we expect errors + if (!$xml_data) { + // build an error message string + $message = ''; + $errors = libxml_get_errors(); + foreach ($errors as $error) { + $message .= t('Line @line, Col @column: @message', array('@line' => $error->line, '@column' => $error->column, '@message' => $error->message)) . "\n\n"; + } + + // clear all libxml errors and restore the old error state + libxml_clear_errors(); + libxml_use_internal_errors($old_error_state); + + // throw an error + services_error($message, 406); + } + // whew, no errors, restore the old error state + libxml_use_internal_errors($old_error_state); + + // unmarshal the SimpleXmlElement, and return the resulting array + $php_array = $this->unmarshalXML($xml_data, NULL); + return (array) $php_array; + } + + /** + * A recusive function that unmarshals an XML string, into a php array. + */ + protected function unmarshalXML($node, $array) { + // For all child XML elements + foreach ($node->children() as $child) { + if (count($child->children()) > 0) { + // if the child has children + $att = 'is_array'; + if ($child->attributes()->$att) { + $new_array = array(); + // recursive through + foreach($child->children() as $item) { + // Make sure that elements with no children gets a value assigned. + $item_keys = array_keys((array) $item); + if (count($item_keys) == 1 && current($item_keys) === 0) { + $new_array[] = (string) $item[0]; + } + elseif (is_object($item)) { + $new_array[] = (string) $item; + } + else { + $new_array[] = self::unmarshalXML($item, $array[$item->getName()]); + } + } + } + else { + // else, simply create an array where the key is name of the element + $new_array = $this->unmarshalXML($child, $array[$child->getName()]); + } + // add $new_array to $array + $array[$child->getName()] = $new_array; + } + else { + // Use the is_raw attribute on value elements for select type fields to + // pass form validation. Example: + // + // + // + // 10513 + // + // + // 10523 + // + // + // + if ($child->attributes()->is_raw) { + return (string) $child; + } + $array[$child->getName()] = (string) $child; + } + } + // return the resulting array + return $array; + } +} + +class ServicesParserJSON implements ServicesParserInterface { + public function parse(ServicesContextInterface $context) { + return json_decode($context->getRequestBody(), TRUE); + } +} + +class ServicesParserFile implements ServicesParserInterface { + public function parse(ServicesContextInterface $context) { + return $context->getRequestBody(); + } +} + +class ServicesParserYAML implements ServicesParserInterface { + public function parse(ServicesContextInterface $context) { + if (($library = libraries_load('spyc')) && !empty($library['loaded'])) { + return Spyc::YAMLLoadString($context->getPostData()); + } + else { + watchdog('REST Server', 'Spyc library not found!', array(), WATCHDOG_ERROR); + return array(); + } + } +} + +class ServicesParserMultipart implements ServicesParserInterface { + public function parse(ServicesContextInterface $context) { + // php://input is not available with enctype="multipart/form-data". + // see http://php.net/manual/en/wrappers.php.php + return $context->getPostData(); + } +} diff --git a/openthess/modules/services/servers/rest_server/includes/ServicesRESTServerFactory.inc b/openthess/modules/services/servers/rest_server/includes/ServicesRESTServerFactory.inc new file mode 100644 index 0000000..7b4623b --- /dev/null +++ b/openthess/modules/services/servers/rest_server/includes/ServicesRESTServerFactory.inc @@ -0,0 +1,105 @@ +data = $data; + } + + public function getRESTServer() { + $content_type_negotiator = $this->getContentTypeNegotiator(); + $context = $this->getContext(); + $resources = $this->getResources(); + $parsers = $this->getParsers(); + $formatters = $this->getFormatters(); + + $class_name = static::$class_name; + return new $class_name($context, $content_type_negotiator, $resources, $parsers, $formatters); + } + + protected function getContentTypeNegotiator() { + return new ServicesContentTypeNegotiator(); + } + + protected function getContext() { + $context = new ServicesContext($this->data['endpoint_path']); + $context->buildFromGlobals(); + return $context; + } + + protected function getResources() { + $endpoint_name = services_get_server_info('endpoint', ''); + $endpoint = services_endpoint_load($endpoint_name); + $resources = services_get_resources($endpoint->name); + module_load_include('inc', 'services', 'includes/services.resource_build'); + _services_apply_endpoint($resources, $endpoint, TRUE); + + return $resources; + } + + protected function getEndpoint() { + $endpoint_name = services_get_server_info('endpoint', ''); + return services_endpoint_load($endpoint_name); + } + + protected function getEndpointSettings() { + static $settings; + + if (empty($settings)) { + $endpoint = $this->getEndpoint(); + + // Get the server settings from the endpoint. + $settings = !empty($endpoint->server_settings) ? $endpoint->server_settings : array(); + // Normalize the settings so that we get the expected structure + // and sensible defaults. + $settings = rest_server_setup_settings($settings); + } + + return $settings; + } + + protected function getParsers() { + $settings = $this->getEndpointSettings(); + + $parsers = rest_server_request_parsers(); + // Remove parsers that have been disabled for this endpoint. + foreach (array_keys($parsers) as $key) { + if (!$settings['parsers'][$key]) { + unset($parsers[$key]); + } + } + + return $parsers; + } + + protected function getFormatters() { + $settings = $this->getEndpointSettings(); + + $formatters = rest_server_response_formatters(); + // Remove formatters that have been disabled for this endpoint. + foreach (array_keys($formatters) as $key) { + if (!$settings['formatters'][$key]) { + unset($formatters[$key]); + } + } + + return $formatters; + } +} diff --git a/openthess/modules/services/servers/rest_server/lib/bencode.php b/openthess/modules/services/servers/rest_server/lib/bencode.php new file mode 100644 index 0000000..c08a6b4 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/lib/bencode.php @@ -0,0 +1,27 @@ + $val) + $out .= bencode($key).bencode($val); + $out .= 'e'; + } else { + $out ='l'; + foreach($element as $val) + $out .= bencode($val); + $out .= 'e'; + } + } + return $out; +} \ No newline at end of file diff --git a/openthess/modules/services/servers/rest_server/lib/mimeparse.php b/openthess/modules/services/servers/rest_server/lib/mimeparse.php new file mode 100644 index 0000000..afba175 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/lib/mimeparse.php @@ -0,0 +1,179 @@ + "0.5" )) + * + * @param string $mime_type + * @return array ($type, $subtype, $params) + */ + public function parse_mime_type($mime_type) { + $parts = explode(";", $mime_type); + + $params = array(); + foreach ($parts as $i=>$param) { + if (strpos($param, '=') !== false) { + list ($k, $v) = explode('=', trim($param)); + $params[$k] = $v; + } + } + + $full_type = trim($parts[0]); + /* Java URLConnection class sends an Accept header that includes a single "*" + Turn it into a legal wildcard. */ + if ($full_type == '*') { + $full_type = '*/*'; + } + list ($type, $subtype) = explode('/', $full_type); + if (!$subtype) throw (new Exception("malformed mime type")); + + return array(trim($type), trim($subtype), $params); + } + + + /** + * Carves up a media range and returns an Array of the + * [type, subtype, params] where "params" is a Hash of all + * the parameters for the media range. + * + * For example, the media range "application/*;q=0.5" would + * get parsed into: + * + * array("application", "*", ( "q", "0.5" )) + * + * In addition this function also guarantees that there + * is a value for "q" in the params dictionary, filling it + * in with a proper default if necessary. + * + * @param string $range + * @return array ($type, $subtype, $params) + */ + public function parse_media_range($range) { + list ($type, $subtype, $params) = $this->parse_mime_type($range); + + if (!(isset($params['q']) && $params['q'] && floatval($params['q']) && + floatval($params['q']) <= 1 && floatval($params['q']) >= 0)) + $params['q'] = '1'; + + return array($type, $subtype, $params); + } + + /** + * Find the best match for a given mime-type against a list of + * media_ranges that have already been parsed by Mimeparser::parse_media_range() + * + * Returns the fitness and the "q" quality parameter of the best match, or an + * array [-1, 0] if no match was found. Just as for Mimeparser::quality(), + * "parsed_ranges" must be an Enumerable of parsed media ranges. + * + * @param string $mime_type + * @param array $parsed_ranges + * @return array ($best_fitness, $best_fit_q) + */ + public function fitness_and_quality_parsed($mime_type, $parsed_ranges) { + $best_fitness = -1; + $best_fit_q = 0; + list ($target_type, $target_subtype, $target_params) = $this->parse_media_range($mime_type); + + foreach ($parsed_ranges as $item) { + list ($type, $subtype, $params) = $item; + + if (($type == $target_type or $type == "*" or $target_type == "*") && + ($subtype == $target_subtype or $subtype == "*" or $target_subtype == "*")) { + + $param_matches = 0; + foreach ($target_params as $k=>$v) { + if ($k != 'q' && isset($params[$k]) && $v == $params[$k]) + $param_matches++; + } + + $fitness = ($type == $target_type) ? 100 : 0; + $fitness += ($subtype == $target_subtype) ? 10 : 0; + $fitness += $param_matches; + + if ($fitness > $best_fitness) { + $best_fitness = $fitness; + $best_fit_q = $params['q']; + } + } + } + + return array( $best_fitness, (float) $best_fit_q ); + } + + /** + * Find the best match for a given mime-type against a list of + * media_ranges that have already been parsed by Mimeparser::parse_media_range() + * + * Returns the "q" quality parameter of the best match, 0 if no match + * was found. This function behaves the same as Mimeparser::quality() except that + * "parsed_ranges" must be an Enumerable of parsed media ranges. + * + * @param string $mime_type + * @param array $parsed_ranges + * @return float $q + */ + public function quality_parsed($mime_type, $parsed_ranges) { + list ($fitness, $q) = $this->fitness_and_quality_parsed($mime_type, $parsed_ranges); + return $q; + } + + /** + * Returns the quality "q" of a mime-type when compared against + * the media-ranges in ranges. For example: + * + * Mimeparser::quality("text/html", "text/*;q=0.3, text/html;q=0.7, + * text/html;level=1, text/html;level=2;q=0.4, *\/*;q=0.5") + * => 0.7 + * + * @param unknown_type $mime_type + * @param unknown_type $ranges + * @return unknown + */ + public function quality($mime_type, $ranges) { + $parsed_ranges = explode(',', $ranges); + + foreach ($parsed_ranges as $i=>$r) + $parsed_ranges[ $i ] = $this->parse_media_range($r); + + return $this->quality_parsed($mime_type, $parsed_ranges); + } + + /** + * Takes a list of supported mime-types and finds the best match + * for all the media-ranges listed in header. The value of header + * must be a string that conforms to the format of the HTTP Accept: + * header. The value of supported is an Enumerable of mime-types + * + * Mimeparser::best_match(array("application/xbel+xml", "text/xml"), "text/*;q=0.5,*\/*; q=0.1") + * => "text/xml" + * + * @param array $supported + * @param string $header + * @return mixed $mime_type or NULL + */ + public function best_match($supported, $header) { + $parsed_header = explode(',', $header); + + foreach ($parsed_header as $i=>$r) + $parsed_header[ $i ] = $this->parse_media_range($r); + + $weighted_matches = array(); + foreach ($supported as $mime_type) { + $weighted_matches[] = array( + $this->fitness_and_quality_parsed($mime_type, $parsed_header), + $mime_type + ); + } + + array_multisort($weighted_matches); + + $a = $weighted_matches[ count($weighted_matches) - 1 ]; + return ( empty( $a[0][1] ) ? null : $a[1] ); + } +} \ No newline at end of file diff --git a/openthess/modules/services/servers/rest_server/rest_server.api.php b/openthess/modules/services/servers/rest_server/rest_server.api.php new file mode 100644 index 0000000..39cb296 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/rest_server.api.php @@ -0,0 +1,60 @@ + $user->name)); + } +} diff --git a/openthess/modules/services/servers/rest_server/rest_server.inc b/openthess/modules/services/servers/rest_server/rest_server.inc new file mode 100644 index 0000000..37f76c2 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/rest_server.inc @@ -0,0 +1,88 @@ + 'checkboxes', + '#title' => t('Response formatters'), + '#required' => TRUE, + '#description' => t('Select the response formats you want to enable for the rest server.'), + ) + _rest_server_settings_checkboxes_attributes($settings['formatters']); + + $form['parsers'] = array( + '#type' => 'checkboxes', + '#title' => t('Request parsing'), + '#required' => TRUE, + '#description' => t('Select the request parser types you want to enable for the rest server.'), + ) + _rest_server_settings_checkboxes_attributes($settings['parsers']); +} + +/** + * Utility function that creates attributes for a checkboxes-type form + * element from a rest server settings array. + * + * @param array $settings + * @return array + */ +function _rest_server_settings_checkboxes_attributes($settings) { + $keys = array_keys($settings); + $options = array_combine($keys, $keys); + $default = array(); + foreach ($settings as $key => $enabled) { + if ($enabled) { + $default[] = $key; + } + } + ksort($options); + return array( + '#options' => $options, + '#default_value' => $default, + ); +} + +/** + * Submit handler for the services REST server settings form. + * + * @param object $endpoint + * The endpoint that's being configured. + * @param array $values + * The partial form-state from services. + * @return array + * The settings for the REST server in this endpoint. + */ +function _rest_server_settings_submit($endpoint, &$values) { + $values['formatters'] = array_map('_rest_server_settings_not_zero', $values['formatters']); + $values['parsers'] = array_map('_rest_server_settings_not_zero', $values['parsers']); + return $values; +} + +/** + * Utility function intended for use with array_map to change everything that + * isn't === 0 into TRUE. + * + * @param string $value + * The value to map. + * @return bool + * FALSE if the $value is === 0 otherwise TRUE is returned. + */ +function _rest_server_settings_not_zero($value) { + return $value !== 0; +} diff --git a/openthess/modules/services/servers/rest_server/rest_server.info b/openthess/modules/services/servers/rest_server/rest_server.info new file mode 100644 index 0000000..a1ea5eb --- /dev/null +++ b/openthess/modules/services/servers/rest_server/rest_server.info @@ -0,0 +1,28 @@ +name = REST Server +description = Provides an REST server. +package = Services - servers + +files[] = rest_server.module + +files[] = includes/RESTServer.inc +files[] = includes/ServicesContentTypeNegotiator.inc +files[] = includes/ServicesRESTServerFactory.inc +files[] = includes/ServicesContext.inc +files[] = includes/ServicesParser.inc +files[] = includes/ServicesFormatter.inc +files[] = tests/ServicesRESTServerTests.test +files[] = tests/rest_server_mock_classes.inc +files[] = lib/bencode.php +files[] = lib/mimeparse.php + +dependencies[] = services +dependencies[] = libraries (>=2.x) + +core = 7.x + +; Information added by Drupal.org packaging script on 2014-01-31 +version = "7.x-3.7" +core = "7.x" +project = "services" +datestamp = "1391207946" + diff --git a/openthess/modules/services/servers/rest_server/rest_server.install b/openthess/modules/services/servers/rest_server/rest_server.install new file mode 100644 index 0000000..69c0e93 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/rest_server.install @@ -0,0 +1,35 @@ + $info) { + $library = libraries_detect($name); + $requirements[$name] = array( + 'title' => $library['name'], + 'severity' => $library['installed'] ? REQUIREMENT_OK : REQUIREMENT_WARNING, + 'value' => $library['installed'] ? l($library['version'], $library['vendor url']) : $library['error message'], + ); + } + } + + return $requirements; +} + +/** + * Implements hook_uninstall(). + */ +function rest_server_uninstall() { + variable_del('rest_server_default_response_format'); +} diff --git a/openthess/modules/services/servers/rest_server/rest_server.module b/openthess/modules/services/servers/rest_server/rest_server.module new file mode 100755 index 0000000..63042fd --- /dev/null +++ b/openthess/modules/services/servers/rest_server/rest_server.module @@ -0,0 +1,195 @@ + 'REST', + 'path' => 'rest', + 'settings' => array( + 'file' => array('inc', 'rest_server'), + 'form' => '_rest_server_settings', + 'submit' => '_rest_server_settings_submit', + ), + ); +} + +/** + * Starting point of the REST server. + * + * @return type + */ +function rest_server_server() { + $endpoint_path = services_get_server_info('endpoint_path', 'services/rest'); + + $services_rest_server_factory = variable_get('services_rest_server_factory_class', 'ServicesRESTServerFactory'); + $rest_server_factory = new $services_rest_server_factory(array('endpoint_path' => $endpoint_path)); + /* @var $rest_server RESTServer */ + $rest_server = $rest_server_factory->getRESTServer(); + + $canonical_path = $rest_server->getCanonicalPath(); + if (empty($canonical_path)) { + $endpoint_name = services_get_server_info('endpoint', ''); + $endpoint = services_endpoint_load($endpoint_name); + return t('Services Endpoint "@name" has been setup successfully.', array('@name' => $endpoint->name)); + } + + try { + return $rest_server->handle(); + } + catch (Exception $e) { + $rest_server->handleException($e); + } +} + +/** + * Builds a list of request parsers that are available to the RESTServer. + * + * @return array + * An associative array of parser callbacks keyed by mime-type. + */ +function rest_server_request_parsers() { + static $parsers = NULL; + if (!$parsers) { + $parsers = array( + 'application/x-www-form-urlencoded' => 'ServicesParserURLEncoded', + 'application/json' => 'ServicesParserJSON', + 'application/vnd.php.serialized' => 'ServicesParserPHP', + 'multipart/form-data' => 'ServicesParserMultipart', + 'application/xml' => 'ServicesParserXML', + 'text/xml' => 'ServicesParserXML', + ); + + if (($library = libraries_load('spyc')) && !empty($library['loaded'])) { + $parsers['application/x-yaml'] = 'ServicesParserYAML'; + } + + drupal_alter('rest_server_request_parsers', $parsers); + } + return $parsers; +} + +/** + * Builds a list of response formatters that are available to the RESTServer. + * + * @return array + * An associative array of formatter info arrays keyed by type extension. The + * formatter info specifies an array of 'mime types' that corresponds to the + * output format; a 'view' class that is a subclass of RESTServerView; and + * 'view arguments' that should be passed to the view when it is created; + */ +function rest_server_response_formatters() { + static $formatters = NULL; + if (!$formatters) { + $formatters = array( + 'xml' => array( + 'mime types' => array('application/xml', 'text/xml'), + 'formatter class' => 'ServicesXMLFormatter', + ), + 'json' => array( + 'mime types' => array('application/json'), + 'formatter class' => 'ServicesJSONFormatter', + ), + 'jsonp' => array( + 'mime types' => array('text/javascript', 'application/javascript'), + 'formatter class' => 'ServicesJSONPFormatter', + ), + 'php' => array( + 'mime types' => array('application/vnd.php.serialized'), + 'formatter class' => 'ServicesPHPFormatter', + ), + 'bencode' => array( + 'mime types' => array('application/x-bencode'), + 'formatter class' => 'ServicesBencodeFormatter', + ), + ); + + if (($library = libraries_load('spyc')) && !empty($library['loaded'])) { + $formatters['yaml'] = array( + 'mime types' => array('text/plain', 'application/x-yaml', 'text/yaml'), + 'formatter class' => 'ServicesYAMLFormatter', + ); + } + + drupal_alter('rest_server_response_formatters', $formatters); + } + return $formatters; +} + +/** + * Set up settings for a rest server endpoint, fills the settings + * array with defaults. This is done to ensure that the default state + * is consistent between what's shown by default in the settings form + * and used by default by the REST server if it hasn't been configured. + * + * @param array $settings + * @return array + * The standardized settings array. + */ +function rest_server_setup_settings($settings = array()) { + // Apply defaults + $settings = $settings + array( + 'formatters' => array('jsonp' => FALSE), + 'parsers' => array('application/x-www-form-urlencoded' => FALSE), + ); + + // Get all available parsers and formatters. + $parsers = rest_server_request_parsers(); + $formatters = rest_server_response_formatters(); + + _rest_server_add_default_and_remove_unknown($settings['parsers'], array_keys($parsers), TRUE); + _rest_server_add_default_and_remove_unknown($settings['formatters'], array_keys($formatters), TRUE); + + return $settings; +} + +/** + * Utility function set set up an array with default values for a set + * of keys and remove all entries that does not match a key in the set. + * + * @param array $array + * The array to modify. + * @param array $keys + * An array of keys. + * @param mixed $default + * A default value. + * @return void + */ +function _rest_server_add_default_and_remove_unknown(&$array, $keys, $default) { + // Add default values to all keys that do not + // exist in $array but exist in $keys. + foreach ($keys as $k) { + if (!isset($array[$k])) { + $array[$k] = $default; + } + } + // Unset all values that key exist in $array + // but does not exist in $keys. + foreach (array_keys($array) as $key) { + if (!in_array($key, $keys)) { + unset($array[$key]); + } + } +} + +/** + * Implements hook_libraries_info(). + */ +function rest_server_libraries_info() { + $libraries['spyc'] = array( + 'name' => 'Spyc', + 'version' => '0.5.1', + 'vendor url' => 'https://github.com/mustangostang/spyc', + 'download url' => 'https://raw2.github.com/mustangostang/spyc/master/Spyc.php', + 'version arguments' => array( + 'file' => 'Spyc.php', + 'pattern' => '@version\s+([0-9a-zA-Z\.-]+)@', + ), + 'files' => array( + 'php' => array('Spyc.php'), + ), + ); + + return $libraries; +} diff --git a/openthess/modules/services/servers/rest_server/tests/ServicesRESTServerTests.test b/openthess/modules/services/servers/rest_server/tests/ServicesRESTServerTests.test new file mode 100644 index 0000000..7c2cce5 --- /dev/null +++ b/openthess/modules/services/servers/rest_server/tests/ServicesRESTServerTests.test @@ -0,0 +1,617 @@ + t('RESTServer unit tests'), + 'description' => t('Test separate methods of class.'), + 'group' => t('Services'), + ); + } + + public function setUp() { + parent::setUp(); + + module_load_include('inc', 'services', 'includes/services.runtime'); + module_load_include('inc', 'rest_server', 'includes/RESTServer'); + module_load_include('inc', 'rest_server', 'includes/ServicesContentTypeNegotiator'); + module_load_include('inc', 'rest_server', 'includes/ServicesContext'); + module_load_include('inc', 'rest_server', 'includes/ServicesParser'); + module_load_include('inc', 'rest_server', 'includes/ServicesFormatter'); + module_load_include('inc', 'rest_server', 'includes/ServicesRESTServerFactory'); + module_load_include('inc', 'rest_server', 'tests/rest_server_mock_classes'); + } + + /** + * Test method getResourceName(). + */ + public function testGetResourceName() { + $factory_args = $this->getDefaultFactoryArguments(); + + $rest_server_factory = new MockServicesRESTServerFactory($factory_args); + $rest_server = $rest_server_factory->getRESTServer(); + + $resource_name = $rest_server->protectedGetResourceName(); + $this->assertEqual($resource_name, 'node', 'Retrieve call resource name determined properly.'); + + + $factory_args['context_data']['get']['q'] = 'rest/user'; + $rest_server_factory = new MockServicesRESTServerFactory($factory_args); + $rest_server = $rest_server_factory->getRESTServer(); + + $resource_name = $rest_server->protectedGetResourceName(); + $this->assertEqual($resource_name, 'user', 'Index call resource name determined properly.'); + } + + /** + * Test method getResponseFormatter(). + */ + public function testGetResponseFormatter() { + $factory_args = $this->getDefaultFactoryArguments(); + + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, $factory_args['formatters']['xml'], 'XML formatter determined properly from URL'); + + // Pass formatter in url that is not recognizable. + $factory_args['context_data']['get']['q'] = 'rest/node/1.php'; + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, FALSE, 'Unknown formatter "php" path extension.'); + + // Default formatter is json. + $factory_args['context_data']['get']['q'] = 'rest/node/1'; + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, $factory_args['formatters']['json'], 'Default is json'); + + // Pass Accept headers. + $factory_args['context_data']['server']['HTTP_ACCEPT'] = 'application/xml'; + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, $factory_args['formatters']['xml'], 'XML formatter determined properly from Accept header "application/xml"'); + + $factory_args['context_data']['server']['HTTP_ACCEPT'] = 'text/xml'; + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, $factory_args['formatters']['xml'], 'XML formatter determined properly from Accept header "text/xml"'); + + $factory_args['context_data']['server']['HTTP_ACCEPT'] = 'text/html;level=2;q=0.7,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, $factory_args['formatters']['xml'], 'XML formatter determined properly from Accept header "text/html;level=2;q=0.7,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"'); + + $factory_args['context_data']['server']['HTTP_ACCEPT'] = 'text/html;level=2;q=0.7,application/json,application/xml;q=0.9,*/*;q=0.8'; + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, $factory_args['formatters']['json'], 'JSON formatter determined properly from Accept header "text/html;level=2;q=0.7,application/json,application/xml;q=0.9,*/*;q=0.8"'); + + $factory_args['context_data']['server']['HTTP_ACCEPT'] = 'text/yaml'; + $formatter = $this->getResponseFormatter($factory_args); + $this->assertEqual($formatter, FALSE, 'Unknown formatter Accept "text/yaml" header.'); + } + + /** + * Test for method resolveController(). + */ + function testResolveController() { + $resource = $this->getTestResource(); + $resource_name = 'node'; + + $factory_args = $this->getDefaultFactoryArguments(); + + // Create operation. + $factory_args['context_data']['request_method'] = 'POST'; + $factory_args['context_data']['get']['q'] = 'rest/node'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['operations']['create'], 'Create operation controller found.'); + + $factory_args['context_data']['request_method'] = 'POST'; + $factory_args['context_data']['get']['q'] = 'rest/node/1.xml'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, FALSE, 'POST request to "node/1.xml" not recognized.'); + + // Retrieve operation. + $factory_args['context_data']['request_method'] = 'GET'; + $factory_args['context_data']['get']['q'] = 'rest/node/1.xml'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['operations']['retrieve'], 'Retrieve operation controller found.'); + + // Update operation. + $factory_args['context_data']['request_method'] = 'PUT'; + $factory_args['context_data']['get']['q'] = 'rest/node/1.xml'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['operations']['update'], 'Update operation controller found.'); + + $factory_args['context_data']['request_method'] = 'PUT'; + $factory_args['context_data']['get']['q'] = 'rest/node'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, FALSE, 'PUT request to "node" not recognized.'); + + // Delete operation. + $factory_args['context_data']['request_method'] = 'DELETE'; + $factory_args['context_data']['get']['q'] = 'rest/node/1.xml'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['operations']['delete'], 'Delete operation controller found.'); + + $factory_args['context_data']['request_method'] = 'DELETE'; + $factory_args['context_data']['get']['q'] = 'rest/node'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, FALSE, 'DELETE request to "node" not recognized.'); + + // Index operation. + $factory_args['context_data']['request_method'] = 'GET'; + $factory_args['context_data']['get']['q'] = 'rest/node'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['operations']['index'], 'Index operation controller found.'); + + // Actions. + $factory_args['context_data']['request_method'] = 'POST'; + $factory_args['context_data']['get']['q'] = 'rest/node/connect'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['actions']['connect'], 'Action "connect" controller found.'); + + $factory_args['context_data']['request_method'] = 'POST'; + $factory_args['context_data']['get']['q'] = 'rest/node/connect/1'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, FALSE, 'POST request to "node/connect/1" not recognized.'); + + $factory_args['context_data']['request_method'] = 'POST'; + $factory_args['context_data']['get']['q'] = 'rest/node/not_connect'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, FALSE, 'POST request to "node/not_connect" not recognized.'); + + // Targeted Actions. + $factory_args['context_data']['request_method'] = 'POST'; + $factory_args['context_data']['get']['q'] = 'rest/node/1/attach_file'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['targeted_actions']['attach_file'], 'Targeted action "attach_file" controller found.'); + + $factory_args['context_data']['request_method'] = 'POST'; + $factory_args['context_data']['get']['q'] = 'rest/node/1/attach_file_fake'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, FALSE, 'POST request to "rest/node/1/attach_file_fake" not recognized.'); + + // Relationships. + $factory_args['context_data']['request_method'] = 'GET'; + $factory_args['context_data']['get']['q'] = 'rest/node/1/files'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, $resource['relationships']['files'], 'Relationship "files" controller found.'); + + $factory_args['context_data']['request_method'] = 'GET'; + $factory_args['context_data']['get']['q'] = 'rest/node/1/files_fake'; + $rest_server = $this->getRESTSever($factory_args); + $controller = $rest_server->protectedResolveController($resource, $resource_name); + $this->assertEqual($controller, FALSE, 'GET request to "rest/node/1/files_fake" not recognized.'); + } + + /** + * Test for parseRequestBody() method. + */ + public function testParseRequestBody() { + $factory_args = $this->getDefaultFactoryArguments(); + $factory_args['parsers'] = array( + 'application/x-www-form-urlencoded' => 'ServicesParserURLEncoded', + 'application/json' => 'ServicesParserJSON', + 'application/vnd.php.serialized' => 'ServicesParserPHP', + 'multipart/form-data' => 'ServicesParserMultipart', + 'application/xml' => 'ServicesParserXML', + 'text/xml' => 'ServicesParserXML', + ); + $factory_args['context_data']['request_method'] = 'POST'; + + $test_data = array('username' => 1, 'password' => 2); + + $factory_args['context_data']['request_body'] = drupal_http_build_query($test_data, '', '&'); + $factory_args['context_data']['server']['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + $rest_server = $this->getRESTSever($factory_args); + $decoded_data = $rest_server->protectedParseRequestBody(); + $this->assertEqual($decoded_data, $test_data, 'URL Enconed parser decoded request data properly.'); + + $factory_args['context_data']['request_body'] = json_encode($test_data); + $factory_args['context_data']['server']['CONTENT_TYPE'] = 'application/json'; + $rest_server = $this->getRESTSever($factory_args); + $decoded_data = $rest_server->protectedParseRequestBody(); + $this->assertEqual($decoded_data, $test_data, 'JSON parser decoded request data properly.'); + + $factory_args['context_data']['request_body'] = serialize($test_data); + $factory_args['context_data']['server']['CONTENT_TYPE'] = 'application/vnd.php.serialized'; + $rest_server = $this->getRESTSever($factory_args); + $decoded_data = $rest_server->protectedParseRequestBody(); + $this->assertEqual($decoded_data, $test_data, 'PHP Serialize parser decoded request data properly.'); + + $factory_args['context_data']['post'] = $test_data; + $factory_args['context_data']['server']['CONTENT_TYPE'] = 'multipart/form-data'; + $rest_server = $this->getRESTSever($factory_args); + $decoded_data = $rest_server->protectedParseRequestBody(); + $this->assertEqual($decoded_data, $test_data, 'Multipart parser decoded request data properly.'); + + $factory_args['context_data']['request_body'] = + ' + 1 + 2 + '; + $factory_args['context_data']['server']['CONTENT_TYPE'] = 'application/xml'; + $rest_server = $this->getRESTSever($factory_args); + $decoded_data = $rest_server->protectedParseRequestBody(); + $this->assertEqual($decoded_data, $test_data, 'XML parser decoded request data properly.'); + + $factory_args['context_data']['server']['CONTENT_TYPE'] = 'application/xml-not-recognized'; + $rest_server = $this->getRESTSever($factory_args); + try { + $decoded_data = $rest_server->protectedParseRequestBody(); + $this->assert(FALSE, 'Unknown Content Type no exception.'); + } + catch (Exception $e) { + $this->assert(TRUE, 'Unknown Content Type thown exception'); + } + } + + /** + * Test for getControllerArgumentsFromSources() method. + */ + public function testGetControllerArgumentsFromSources() { + $test_resources = $this->getTestResource(); + $factory_args = $this->getDefaultFactoryArguments(); + $rest_server = $this->getRESTSever($factory_args); + + $retrieve_controller = $test_resources['operations']['retrieve']; + + $sources = array( + 'path' => array('1'), + 'param' => array(), + 'data' => array(), + 'headers' => array(), + ); + + // Arguments from path. + $arguments = $rest_server->protectedGetControllerArgumentsFromSources($retrieve_controller, $sources); + $this->assertEqual($arguments, array(0 => 1), 'Path argument retrieved.'); + +// $sources['path'][0] = $this->randomName(); +// $arguments = $rest_server->protectedGetControllerArgumentsFromSources($retrieve_controller, $sources); +// $this->assertEqual($arguments, array(0 => 0), 'Path argument retrieved and converted to integer.'); + + // Arguments from 'data' + $create_controller = $test_resources['operations']['create']; + + $sources['data'] = array(1, 2); + $arguments = $rest_server->protectedGetControllerArgumentsFromSources($create_controller, $sources); + $this->assertEqual($arguments, array($sources['data']), 'Data argument that is array retrieved.'); + + $sources['data'] = $this->randomName(); + $arguments = $rest_server->protectedGetControllerArgumentsFromSources($create_controller, $sources); + $this->assertEqual($arguments, array(array($sources['data'])), 'Data argument that is string retrieved and converted to array.'); + + unset($create_controller['args'][0]['type']); + $arguments = $rest_server->protectedGetControllerArgumentsFromSources($create_controller, $sources); + $this->assertEqual($arguments, array($sources['data']), 'Data argument that is string retrieved and converted to array.'); + + // Arguments from 'get' + $create_controller['args'][0]['source'] = array('get' => 'node'); + $sources['data'] = array(); + $sources['get'] = array('node' => $this->randomString()); + $arguments = $rest_server->protectedGetControllerArgumentsFromSources($create_controller, $sources); + $this->assertEqual($arguments, array($sources['get']['node']), 'Get argument retrieved.'); + + // Arguments combination. + $sources['data'] = array('comment' => $this->randomString()); + $create_controller['args'][] = array( + 'name' => 'comment', + 'optional' => FALSE, + 'source' => array('data' => 'comment'), + 'type' => 'string', + ); + $arguments = $rest_server->protectedGetControllerArgumentsFromSources($create_controller, $sources); + $this->assertEqual($arguments, array($sources['get']['node'], $sources['data']['comment']), 'Get and Data arguments retrieved.'); + + // Required argument. + $sources['get'] = array(); + try { + $rest_server->protectedGetControllerArgumentsFromSources($create_controller, $sources); + $this->assert(FALSE, 'Missing required argument no error thrown.'); + } + catch (Exception $e) { + $this->assert(TRUE, 'Missing required argument error thrown.'); + } + } + + /** + * Test for render() method. + */ + public function testRender() { + $factory_args = $this->getDefaultFactoryArguments(); + $rest_server = $this->getRESTSever($factory_args); + + $formatters = array( + 'xml' => array( + 'mime types' => array('application/xml', 'text/xml'), + 'formatter class' => 'ServicesXMLFormatter', + ), + 'json' => array( + 'mime types' => array('application/json'), + 'formatter class' => 'ServicesJSONFormatter', + ), + 'php' => array( + 'mime types' => array('application/vnd.php.serialized'), + 'formatter class' => 'ServicesPHPFormatter', + ), + ); + $result = array('a' => 'b', 1, FALSE); + + + $xml_formatter = $formatters['xml']; + $formatted_output = $rest_server->protectedRender($xml_formatter, $result); + $expected_formatted_output = + '' . "\n" . + 'b1' . "\n"; + $this->assertEqual($formatted_output, $expected_formatted_output, 'XML formatter encoded correctly.'); + + $json_formatter = $formatters['json']; + $formatted_output = $rest_server->protectedRender($json_formatter, $result); + $this->assertEqual($formatted_output, '{"a":"b","0":1,"1":false}', 'JSON formatter encoded correctly.'); + + $php_formatter = $formatters['php']; + $formatted_output = $rest_server->protectedRender($php_formatter, $result); + $this->assertEqual($formatted_output, 'a:3:{s:1:"a";s:1:"b";i:0;i:1;i:1;b:0;}', 'PHP formatter encoded correctly.'); + } + + protected function getRESTSever($factory_args) { + $rest_server_factory = new MockServicesRESTServerFactory($factory_args); + return $rest_server_factory->getRESTServer(); + } + + protected function getResponseFormatter($factory_args) { + try { + $rest_server = $this->getRESTSever($factory_args); + return $rest_server->protectedGetResponseFormatter(); + } + catch (Exception $e) { + return FALSE; + } + } + + protected function getDefaultFactoryArguments() { + $context_data = array( + 'get' => array('q' => 'rest/node/1.xml'), + 'server' => array(), + 'post' => array(), + 'request_body' => '', + ); + + $formatters = array( + 'xml' => array( + 'mime types' => array('application/xml', 'text/xml'), + 'formatter class' => 'ServicesXMLFormatter', + ), + 'json' => array( + 'mime types' => array('application/json'), + 'formatter class' => 'ServicesJSONFormatter', + ), + ); + + $factory_args = array( + 'endpoint_path' => 'rest', + 'context_data' => $context_data, + 'formatters' => $formatters, + ); + + return $factory_args; + } + + protected function getTestResource() { + return array( + 'operations' => array( + 'retrieve' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_retrieve', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to get', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + ), + 'create' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_create', + 'args' => array( + array( + 'name' => 'node', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The node data to create', + 'type' => 'array', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('create'), + 'access arguments append' => TRUE, + ), + 'update' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_update', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to get', + ), + array( + 'name' => 'node', + 'optional' => FALSE, + 'source' => 'data', + 'description' => 'The node data to update', + 'type' => 'array', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('update'), + 'access arguments append' => TRUE, + ), + 'delete' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_delete', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + ), + ), + 'access callback' => '_node_resource_access', + 'access arguments' => array('delete'), + 'access arguments append' => TRUE, + ), + 'index' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_index', + 'args' => array( + array( + 'name' => 'page', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'The zero-based index of the page to get, defaults to 0.', + 'default value' => 0, + 'source' => array('param' => 'page'), + ), + array( + 'name' => 'fields', + 'optional' => TRUE, + 'type' => 'string', + 'description' => 'The fields to get.', + 'default value' => '*', + 'source' => array('param' => 'fields'), + ), + array( + 'name' => 'parameters', + 'optional' => TRUE, + 'type' => 'array', + 'description' => 'Parameters array', + 'default value' => array(), + 'source' => array('param' => 'parameters'), + ), + array( + 'name' => 'pagesize', + 'optional' => TRUE, + 'type' => 'int', + 'description' => 'Number of records to get per page.', + 'default value' => variable_get('services_node_index_page_size', 20), + 'source' => array('param' => 'pagesize'), + ), + ), + 'access arguments' => array('access content'), + ), + ), + 'targeted_actions' => array( + 'attach_file' => array( + 'help' => 'Upload and attach file(s) to a node. POST multipart/form-data to node/123/attach_file', + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'callback' => '_node_resource_attach_file', + 'access callback' => '_node_resource_access', + 'access arguments' => array('update'), + 'access arguments append' => TRUE, + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node to attach a file to', + ), + array( + 'name' => 'field_name', + 'optional' => FALSE, + 'source' => array('data' => 'field_name'), + 'description' => 'The file parameters', + 'type' => 'string', + ), + array( + 'name' => 'attach', + 'optional' => TRUE, + 'source' => array('data' => 'attach'), + 'description' => 'Attach the file(s) to the node. If FALSE, this clears ALL files attached, and attaches the files', + 'type' => 'int', + 'default value' => TRUE, + ), + array( + 'name' => 'field_values', + 'optional' => TRUE, + 'source' => array('data' => 'field_values'), + 'description' => 'The extra field values', + 'type' => 'array', + 'default value' => array(), + ), + ), + ), + ), + 'relationships' => array( + 'files' => array( + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/node_resource'), + 'help' => t('This method returns files associated with a node.'), + 'access callback' => '_node_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'callback' => '_node_resource_load_node_files', + 'args' => array( + array( + 'name' => 'nid', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'The nid of the node whose files we are getting', + ), + array( + 'name' => 'file_contents', + 'type' => 'int', + 'description' => t('To return file contents or not.'), + 'source' => array('path' => 2), + 'optional' => TRUE, + 'default value' => TRUE, + ), + array( + 'name' => 'image_styles', + 'type' => 'int', + 'description' => t('To return image styles or not.'), + 'source' => array('path' => 3), + 'optional' => TRUE, + 'default value' => FALSE, + ), + ), + ), + ), + 'actions' => array( + 'connect' => array( + 'access callback' => 'services_access_menu', + 'help' => t('Returns the details of currently logged in user.'), + 'file' => array('type' => 'inc', 'module' => 'services', 'name' => 'resources/system_resource'), + 'callback' => '_system_resource_connect', + ), + ), + ); + } +} diff --git a/openthess/modules/services/servers/rest_server/tests/rest_server_mock_classes.inc b/openthess/modules/services/servers/rest_server/tests/rest_server_mock_classes.inc new file mode 100644 index 0000000..150afed --- /dev/null +++ b/openthess/modules/services/servers/rest_server/tests/rest_server_mock_classes.inc @@ -0,0 +1,78 @@ +data['endpoint_path']); + $context->setData($this->data['context_data']); + return $context; + } + + protected function getResources() { + if (isset($this->data['resources'])) { + return $this->data['resources']; + } + return array(); + } + + protected function getFormatters() { + if (isset($this->data['formatters'])) { + return $this->data['formatters']; + } + return array(); + } + + protected function getParsers() { + if (isset($this->data['parsers'])) { + return $this->data['parsers']; + } + return array(); + } +} + +/** + * Mock ServicesContext object. + */ +class MockServicesContext extends ServicesContext { + public function setData($data) { + $this->data = array_merge($this->data, $data); + } +} + +/** + * Extend RESTServer to test protected methods. + */ +class MockRESTServer extends RESTServer { + public function protectedGetControllerArgumentsFromSources($controller, $sources) { + return $this->getControllerArgumentsFromSources($controller, $sources); + } + + public function protectedRender($formatter, $result) { + return $this->render($formatter, $result); + } + + public function protectedGetResourceName() { + return $this->getResourceName(); + } + + public function protectedGetResponseFormatter() { + return $this->getResponseFormatter(); + } + + public function protectedResolveController($resource, &$operation) { + return $this->resolveController($resource, $operation); + } + + public function protectedParseRequestBody() { + return $this->parseRequestBody(); + } +} \ No newline at end of file diff --git a/openthess/modules/services/servers/xmlrpc_server/xmlrpc_server.info b/openthess/modules/services/servers/xmlrpc_server/xmlrpc_server.info new file mode 100644 index 0000000..f1acb2c --- /dev/null +++ b/openthess/modules/services/servers/xmlrpc_server/xmlrpc_server.info @@ -0,0 +1,15 @@ +name = XMLRPC Server +description = Provides a XMLRPC server. +package = Services - servers + +files[] = xmlrpc_server.module + +dependencies[] = services + +core = 7.x +; Information added by Drupal.org packaging script on 2014-01-31 +version = "7.x-3.7" +core = "7.x" +project = "services" +datestamp = "1391207946" + diff --git a/openthess/modules/services/servers/xmlrpc_server/xmlrpc_server.module b/openthess/modules/services/servers/xmlrpc_server/xmlrpc_server.module new file mode 100644 index 0000000..a2f81f7 --- /dev/null +++ b/openthess/modules/services/servers/xmlrpc_server/xmlrpc_server.module @@ -0,0 +1,83 @@ +.. So the node + * resource's retrieve method has an XMLRPC procedure name of node.retrieve, + * the user resource's login action has an XMLRPC procedure name of + * user.login, etc. + */ + +/** + * Implementation of hook_server_info(). + */ +function xmlrpc_server_server_info() { + return array( + 'name' => 'XMLRPC', + ); +} + +/** + * Implementation of hook_server(). + */ +function xmlrpc_server_server() { + require_once './includes/xmlrpc.inc'; + require_once './includes/xmlrpcs.inc'; + + return xmlrpc_server(array_merge(xmlrpc_server_xmlrpc(), module_invoke_all('xmlrpc'))); +} + +/** + * Return an array of all defined services methods and callbacks. + * + * @see xmlrpc_server() + */ +function xmlrpc_server_xmlrpc() { + $callbacks = array(); + module_load_include('inc', 'services', 'includes/services.runtime'); + $resources = services_get_resources(services_get_server_info('endpoint', '')); + if (!empty($resources)) { + // Translate all resources + foreach ($resources as $name => $def) { + foreach (services_resources_as_procedures($def, $name) as $method) { + $callbacks[$method['method']] = 'xmlrpc_server_call_wrapper'; + } + } + } + + return $callbacks; +} + +/** + * Pass XMLRPC server requests to the appropriate services method. + * + * This function can take varying parameters as are appropriate to + * the service in question. + */ +function xmlrpc_server_call_wrapper() { + $xmlrpc_server = xmlrpc_server_get(); + $method_name = $xmlrpc_server->message->methodname; + $args = func_get_args(); + $endpoint = services_get_server_info('endpoint', ''); + $controller = services_controller_get($method_name, $endpoint); + try { + // Add in default arguments if arguments still dont exist. + if (isset($controller['args']) && is_array($controller['args'])) { + foreach ($controller['args'] as $index => $arg) { + if ($arg['optional'] && isset($arg['default value']) && !isset($args[$index])) { + $args[$index] = $arg['default value']; + } + elseif ($arg['optional'] == FALSE && !isset($args[$index])) { + return services_error(t('Missing required argument @arg', array('@arg' => $arg['name'])), 401); + } + } + } + return services_controller_execute($controller, $args); + } + catch (Exception $e) { + return xmlrpc_error($e->getCode(), $e->getMessage()); + } +} diff --git a/openthess/modules/services/services.admin.inc b/openthess/modules/services/services.admin.inc new file mode 100644 index 0000000..19ce413 --- /dev/null +++ b/openthess/modules/services/services.admin.inc @@ -0,0 +1,126 @@ + array('select-all')), + array('data' => t('Resource'), 'class' => array('resource_method')), + array('data' => t('Settings'), 'class' => array('resource_settings')), + array('data' => t('Alias'), 'class' => array('resource_alias')), + ); + + // Define the images used to expand/collapse the method groups. + $js = array( + 'images' => array( + 'collapsed' => theme('image', array('path' => 'misc/menu-collapsed.png', 'alt' => t('Expand'), 'title' => t('Expand'))) . ' (' . t('Expand') . ')', + 'expanded' => theme('image', array('path' => 'misc/menu-expanded.png', 'alt' => t('Collapse'), 'title' => t('Collapse'))) . ' (' . t('Collapse') . ')', + ), + ); + + // Cycle through each method group and create a row. + $rows = array(); + foreach (element_children($table) as $key) { + $element = &$table[$key]; + $row = array(); + + // Make the class name safe for output on the page by replacing all + // non-word/decimal characters with a dash (-). + $method_class = strtolower(trim(preg_replace("/[^\w\d]/", "-", $key))); + + // Select the right "expand"/"collapse" image, depending on whether the + // category is expanded (at least one method selected) or not. + $collapsed = !empty($element['#collapsed']); + + // Place-holder for checkboxes to select group of methods. + $row[] = array('id' => $method_class, 'class' => array('resource-select-all')); + + // Expand/collapse image and group title. + $row[] = array( + 'data' => '
' . + '', + 'class' => array('resource-group-label'), + ); + + $row[] = array( + 'data' => ' ', + 'class' => array('resource-group-description'), + ); + $row[] = array( + 'data' => drupal_render($element['alias']), + 'class' => array('resource-group-alias'), + ); + $rows[] = array('data' => $row, 'class' => array('resource-group')); + + // Add individual methods to group. + $current_js = array( + 'methodClass' => $method_class . '-method', + 'collapsed' => $collapsed, + 'clickActive' => FALSE, + ); + + // Cycle through each method within the current group. + foreach (element_children($element) as $class) { + if($class != 'alias') { + $class_element = $element[$class]; + + // Add group (class) header row. + $rows[] = array('data' => array(NULL, array( + 'data' => '', + 'class' => array('resource-operation-class'), + ), NULL, NULL), 'class' => array($method_class . '-method', 'resource-operation-class')); + + foreach (element_children($class_element) as $op_name) { + $row = array(); + $method = $class_element[$op_name]; + + // Store method title and description so that checkbox won't render them. + $title = $method['#title']; + $description = $method['#description']; + + $method['#title_display'] = 'invisible'; + $method['enabled']['#title_display'] = 'invisible'; + unset($method['#description']); + + // Test name is used to determine what methods to run. + $method['#name'] = $class; + + $row[] = array( + 'data' => drupal_render($method['enabled']), + 'class' => array('resource-method-select'), + ); + $row[] = array( + 'data' => '' . '
' . $description . '
', + 'class' => array('resource-method-description'), + ); + $row[] = array( + 'data' => drupal_render($method['settings']), + 'class' => array('resource-method-settings'), + ); + $row[] = array( + 'data' => '
 
', + 'class' => array('resource-method-alias'), + ); + $rows[] = array('data' => $row, 'class' => array($method_class . '-method', 'resource-method')); + } + } + + } + $js['resources'][$method_class] = $current_js; + unset($table[$key]); + } + + // Add js array of settings. + drupal_add_js(array('services' => $js), 'setting'); + + if (empty($rows)) { + return '' . t('No resourcess to display.') . ''; + } + else { + return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'resource-form-table'))); + } +} \ No newline at end of file diff --git a/openthess/modules/services/services.info b/openthess/modules/services/services.info new file mode 100644 index 0000000..052c4bc --- /dev/null +++ b/openthess/modules/services/services.info @@ -0,0 +1,36 @@ +name = Services +description = Provide an API for creating web services. +package = Services +core = 7.x +php = 5.x +configure = admin/structure/services + +files[] = includes/services.runtime.inc +files[] = tests/functional/NoAuthEndpointTestRunner.test +files[] = tests/functional/ServicesResourceNodeTests.test +files[] = tests/functional/ServicesResourceUserTests.test +files[] = tests/functional/ServicesResourceSystemTests.test +files[] = tests/functional/ServicesResourceCommentTests.test +files[] = tests/functional/ServicesResourceTaxonomyTests.test +files[] = tests/functional/ServicesResourceFileTests.test +files[] = tests/functional/ServicesResourceDisabledTests.test +files[] = tests/functional/ServicesEndpointTests.test +files[] = tests/functional/ServicesParserTests.test +files[] = tests/functional/ServicesAliasTests.test +files[] = tests/functional/ServicesXMLRPCTests.test +files[] = tests/functional/ServicesVersionTests.test +files[] = tests/functional/ServicesArgumentsTests.test +files[] = tests/functional/ServicesSecurityTests.test +files[] = tests/unit/ServicesSpycLibraryTests.test +files[] = tests/ui/ServicesUITests.test +files[] = tests/services.test + +dependencies[] = ctools + + +; Information added by Drupal.org packaging script on 2014-01-31 +version = "7.x-3.7" +core = "7.x" +project = "services" +datestamp = "1391207946" + diff --git a/openthess/modules/services/services.install b/openthess/modules/services/services.install new file mode 100644 index 0000000..ec7be68 --- /dev/null +++ b/openthess/modules/services/services.install @@ -0,0 +1,237 @@ + 'Stores endpoint information for services', + 'fields' => array( + 'eid' => array( + 'type' => 'serial', + 'description' => 'Primary ID field for the table. Not used for anything except internal lookups.', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'no export' => TRUE, + ), + 'name' => array( + 'description' => 'The name of the endpoint.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'server' => array( + 'description' => 'The name of the server used in this endpoint.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'path' => array( + 'description' => 'The path to the endpoint for this server.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'authentication' => array( + 'description' => 'The authentication settings for the endpoint.', + 'type' => 'text', + 'size' => 'big', + 'not null' => TRUE, + 'serialize' => TRUE, + 'object default' => array(), + ), + 'server_settings' => array( + 'description' => 'The server settings for the endpoint.', + 'type' => 'blob', + 'size' => 'big', + 'not null' => TRUE, + 'serialize' => TRUE + ), + 'resources' => array( + 'description' => 'Information about the resources exposed in this endpoint.', + 'type' => 'text', + 'size' => 'big', + 'not null' => TRUE, + 'serialize' => TRUE, + 'object default' => array(), + ), + 'debug' => array( + 'description' => 'Set the endpoint in debug mode.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0 + ), + ), + 'primary key' => array('eid'), + 'unique keys' => array( + 'name' => array('name'), + ), + 'export' => array( + 'key' => 'name', + 'identifier' => 'endpoint', + 'primary key' => 'name', + 'api' => array( + 'owner' => 'services', + 'api' => 'services', + 'minimum_version' => 3, + 'current_version' => 3, + ), + ), + ); + + return $schema; +} + +/** + * Implements hook_requirements(). + */ +function services_requirements($phase) { + $requirements = array(); + $t = get_t(); + // Warn users of the possible threat. + if ($phase == 'runtime') { + //Pull endpoints that do not have services authentication enabled + $result = db_query('SELECT * FROM {services_endpoint} AS se WHERE se.authentication NOT LIKE :services', array(':services' => '%services%')); + $items = array(); + // Build our items array + foreach ($result as $endpoint) { + $items[] = l($endpoint->name, 'admin/structure/services/list/'. $endpoint->name .'/edit'); + } + // theme the endpoints list + $endpoints = ''; + if (!empty($items)) { + $endpoints = theme('item_list', array('items' => $items)); + } + // Only display the list if we have at least one endpoint without services authentication. + if (count($items)) { + $requirements['services'] = array( + 'description' => $t('Services authentication mechanism has not been enabled for the following endpoints. Requests to these endpoints will always be anonymous.'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $endpoints, + 'title' => 'Services Authentication Mechanism', + ); + } else { + $requirements['services'] = array( + 'severity' => REQUIREMENT_OK, + 'value' => 'Enabled for all Endpoints', + 'title' => 'Services Authentication Mechanism', + ); + } + } + + return $requirements; +} + +/** + * Implements hook_uninstall(). + */ +function services_uninstall() { + $ret = array(); + + // Drop legacy tables + $legacy_tables = array('services_keys', 'services_timestamp_nonce'); + foreach ($legacy_tables as $table) { + if (db_table_exists($table)) { + db_drop_table($ret, $table); + } + } + + variable_del('services_use_key'); + variable_del('services_use_sessid'); + variable_del('services_debug'); + variable_del('services_auth_module'); +} + +/** + * Update 7301 adds debugging to each endopint to facilitate easier development + */ +function services_update_7301() { + $table = 'services_endpoint'; + foreach (array('debug' => 0, 'status' => 1) as $field => $value) { + if (!db_field_exists($table, $field)) { + db_add_field($table, $field, array('type' => 'int', 'not null' => TRUE, 'default' => $value)); + } + } +} + +/** + * Update 7302 restores the usage of Chaos tools to check for enable/disable-status + */ +function services_update_7302() { + $table = 'services_endpoint'; + if (db_field_exists($table, 'status')) { + db_drop_field($table, 'status'); + } +} + +/** + * Update 7303 adds the possibility to configure server settings on a per-endpoint basis. + * and sets upgrades all new servers to have at least services session enabled. + */ +function services_update_7303() { + // Add the new server settings field. + if (!db_field_exists('services_endpoint', 'server_settings')) { + db_add_field('services_endpoint', 'server_settings', array( + 'description' => 'The server settings for the endpoint.', + 'type' => 'blob', + 'size' => 'big', + 'not null' => TRUE, + 'serialize' => TRUE, + 'initial' => '', + )); + } + // Fetch all endpoints that currently exist + $result = db_select('services_endpoint', 'se') + ->fields('se') + ->execute() + ->fetchAll(); + // Loop through every endpoint and update the authentication section. + // Note, this will not remove previous authentication settings, it will + // only add to them. + foreach($result as $services_endpoint_object) { + $new_authentication = array( + 'services' => 'services', + ); + $unserial_endpoint_settings = unserialize($services_endpoint_object->authentication); + db_update('services_endpoint') + ->fields(array('authentication' => serialize(array_merge($unserial_endpoint_settings, $new_authentication)))) + ->condition('eid', $services_endpoint_object->eid) + ->execute(); + } +} +/** + * Update 7304 removes the title as it is no longer used + */ +function services_update_7399() { + $table = 'services_endpoint'; + if (db_field_exists($table, 'title')) { + db_drop_field($table, 'title'); + } +} + +/** + * Update 7400 reduces nesting in the way server settings are stored + */ +function services_update_7400() { + module_load_include('module','services'); + $server_names = array_keys(services_get_servers()); + foreach (services_endpoint_load_all() as $endpoint) { + $settings = $endpoint->server_settings; + if (!empty($settings)) { + if (in_array(key($settings), $server_names)) { + $settings = current($settings); + } + } + else { + $settings = array(); + } + $endpoint->server_settings = $settings; + services_endpoint_save($endpoint); + } +} diff --git a/openthess/modules/services/services.make b/openthess/modules/services/services.make new file mode 100644 index 0000000..5168365 --- /dev/null +++ b/openthess/modules/services/services.make @@ -0,0 +1,8 @@ +api = 2 +core = 7.x + +; Libraries +libraries[spyc][directory_name] = spyc +libraries[spyc][download][type] = file +libraries[spyc][download][url] = https://raw2.github.com/mustangostang/spyc/master/Spyc.php +libraries[spyc][type] = library diff --git a/openthess/modules/services/services.module b/openthess/modules/services/services.module new file mode 100644 index 0000000..eba8cb8 --- /dev/null +++ b/openthess/modules/services/services.module @@ -0,0 +1,849 @@ +' . t('Visit the Services Handbook for help and information.', array('@handbook_url' => 'http://drupal.org/node/109782')) . '

'; + break; + case 'admin/structure/services': + $output = '

' . t('Services are collections of methods available to remote applications. They are defined in modules, and may be accessed in a number of ways through server modules. Visit the Services Handbook for help and information.', array('@handbook_url' => 'http://drupal.org/node/109782')) . '

'; + $output .= '

' . t('All enabled services and methods are shown. Click on any method to view information or test.') . '

'; + break; + } + + return $output; +} + +/** + * Implements hook_perm(). + */ +function services_permission() { + return array( + 'administer services' => array( + 'title' => t('Administer services'), + 'description' => t('Configure and setup services module.'), + ), + // File resource permissions + 'get any binary files' => array( + 'title' => t('Get any binary files'), + 'description' => t(''), + ), + 'get own binary files' => array( + 'title' => t('Get own binary files'), + 'description' => t(''), + ), + 'save file information' => array( + 'title' => t('Save file information'), + 'description' => t(''), + ), + // System resource permissions + 'get a system variable' => array( + 'title' => t('Get a system variable'), + 'description' => t(''), + ), + 'set a system variable' => array( + 'title' => t('Set a system variable'), + 'description' => t(''), + ), + // Query-limiting permissions + 'perform unlimited index queries' => array( + 'title' => t('Perform unlimited index queries'), + 'description' => t('This permission will allow user to perform index queries with unlimited number of results.'), + ), + ); +} + +/** + * Implements hook_hook_info(). + */ +function services_hook_info() { + $hooks['services_resources'] = array( + 'group' => 'services', + ); + return $hooks; +} + +/** + * Implements hook_menu(). + * + * Services UI is defined in the export-ui plugin. + */ +function services_menu() { + $items = array(); + if (module_exists('ctools')) { + $endpoints = services_endpoint_load_all(); + foreach ($endpoints as $endpoint) { + if (empty($endpoint->disabled)) { + $items[$endpoint->path] = array( + 'title' => 'Services endpoint', + 'access callback' => 'services_access_menu', + 'page callback' => 'services_endpoint_callback', + 'page arguments' => array($endpoint->name), + 'type' => MENU_CALLBACK, + ); + } + } + } + + $items['services/session/token'] = array( + 'page callback' => '_services_session_token', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implements of hook_ctools_plugin_api(). + */ +function services_ctools_plugin_api($module, $api) { + if ($module == 'services' && $api == 'plugins') { + return array('version' => 3); + } +} + +/** + * Implement of hook_ctools_plugin_directory(). + */ +function services_ctools_plugin_directory($module, $type) { + // Safety: go away if CTools is not at an appropriate version. + if (!module_invoke('ctools', 'api_version', SERVICES_REQUIRED_CTOOLS_API)) { + return; + } + if ($type =='export_ui') { + return 'plugins/export_ui'; + } +} + +/** + * Access callback that always returns TRUE. + * + * This callback is necessary for services like login and logout that should + * always be wide open and accessible. + * + * *** USE THIS WITH GREAT CAUTION *** + * + * If you think you need it you are almost certainly wrong. + */ +function services_access_menu() { + return TRUE; +} + +/** + * Implements hook_theme(). + */ +function services_theme() { + return array( + 'services_endpoint_index' => array( + 'template' => 'services_endpoint_index', + 'arguments' => array('endpoints' => NULL), + ), + 'services_resource_table' => array( + 'render element' => 'table', + 'file' => 'services.admin.inc', + ), + ); +} + +/** + * Returns information about the installed server modules on the system. + * + * @return array + * An associative array keyed after module name containing information about + * the installed server implementations. + */ +function services_get_servers($reset = FALSE) { + $servers = &drupal_static(__FUNCTION__); + + if (!$servers || $reset) { + $servers = array(); + foreach (module_implements('server_info') as $module) { + if ($module != 'sqlsrv') { + $servers[$module] = call_user_func($module . '_server_info'); + } + } + } + + return $servers; +} + +/** + * Menu system page callback for server endpoints. + * + * @param string $endpoint + * The endpoint name. + * @return void + */ +function services_endpoint_callback($endpoint_name) { + module_load_include('inc', 'services', 'includes/services.runtime'); + + // Explicitly set the title to avoid expensive menu calls in token + // and elsewhere. + if (!($title = drupal_set_title())) { + drupal_set_title('Services endpoint'); + } + + $endpoint = services_endpoint_load($endpoint_name); + $server = $endpoint->server; + + if (function_exists($server . '_server')) { + // call the server + services_set_server_info_from_array(array( + 'module' => $server, + 'endpoint' => $endpoint_name, + 'endpoint_path' => $endpoint->path, + 'debug' => $endpoint->debug, + 'settings' => $endpoint->server_settings, + )); + if ($endpoint->debug) { + watchdog('services', 'Calling server: %server', array('%server' => $server . '_server'), WATCHDOG_DEBUG); + watchdog('services', 'Server info main object:
@info
', array('@info' => print_r(services_server_info_object(), TRUE)), WATCHDOG_DEBUG); + } + print call_user_func($server . '_server'); + + // Do not let this output + drupal_page_footer(); + exit(); + } + // return 404 if the server doesn't exist + drupal_not_found(); +} + + + +/** + * Create a new endpoint with defaults appropriately set from schema. + * + * @return stdClass + * An endpoint initialized with the default values. + */ +function services_endpoint_new() { + ctools_include('export'); + return ctools_export_new_object('services_endpoint'); +} + +/** + * Load a single endpoint. + * + * @param string $name + * The name of the endpoint. + * @return stdClass + * The endpoint configuration. + */ +function services_endpoint_load($name) { + ctools_include('export'); + $result = ctools_export_load_object('services_endpoint', 'names', array($name)); + if (isset($result[$name])) { + return $result[$name]; + } + return FALSE; +} + +/** + * Load all endpoints. + * + * @return array + * Array of endpoint objects keyed by endpoint names. + */ +function services_endpoint_load_all() { + ctools_include('export'); + return ctools_export_load_object('services_endpoint'); +} + +/** + * Saves an endpoint in the database. + * + * @return void + */ +function services_endpoint_save($endpoint) { + if (is_array($endpoint) && isset($endpoint['build_info'])) { + $endpoint = $endpoint['build_info']['args'][0]; + } + + // Set a default of an array if the value is not present. + foreach (array('server_settings', 'resources', 'authentication') as $endpoint_field) { + if (empty($endpoint->{$endpoint_field})) { + $endpoint->{$endpoint_field} = array(); + } + } + + ctools_export_crud_save('services_endpoint', $endpoint); + ctools_export_load_object_reset('services_endpoint'); + menu_rebuild(); + cache_clear_all('services:' . $endpoint->name . ':', 'cache', TRUE); +} + +/** + * Remove an endpoint. + * + * @return void + */ +function services_endpoint_delete($endpoint) { + ctools_export_crud_delete('services_endpoint', $endpoint); + ctools_export_load_object_reset('services_endpoint'); + menu_rebuild(); + cache_clear_all('services:' . $endpoint->name . ':', 'cache', TRUE); +} + +/** + * Export an endpoint. + * + * @return string + */ +function services_endpoint_export($endpoint, $indent = '') { + ctools_include('export'); + return ctools_export_object('services_endpoint', $endpoint, $indent); +} + + +/** + * Gets all resource definitions. + * + * @param string $endpoint_name + * Optional. The endpoint endpoint that's being used. + * @return array + * An array containing all resources. + */ +function services_get_resources($endpoint_name = '') { + $cache_key = 'services:' . $endpoint_name . ':resources'; + + $resources = array(); + if (($cache = cache_get($cache_key)) && isset($cache->data)) { + $resources = $cache->data; + } + else { + module_load_include('inc', 'services', 'includes/services.resource_build'); + $resources = _services_build_resources($endpoint_name); + cache_set($cache_key, $resources); + } + + return $resources; +} + +/** + * Load the resources of the endpoint. + * + * @return array + */ +function services_get_resources_apply_settings($endpoint_name) { + $resources = services_get_resources($endpoint_name); + module_load_include('inc', 'services', 'includes/services.resource_build'); + $endpoint = services_endpoint_load($endpoint_name); + _services_apply_endpoint($resources, $endpoint, TRUE); + + return $resources; +} + +/** + * Returns information about resource API version information. + * The resource API is the way modules expose resources to services, + * not the API that is exposed to the consumers of your services. + * + * @return array + * API version information. 'default_version' is the version that's assumed + * if the module doesn't declare an API version. 'versions' is an array + * containing the known API versions. 'current_version' is the current + * version number. + */ +function services_resource_api_version_info() { + $info = array( + 'default_version' => 3001, + 'versions' => array(3002), + ); + $info['current_version'] = max($info['versions']); + return $info; +} + +/** + * Implements hook_services_resources(). + */ +function services_services_resources() { + module_load_include('inc', 'services', 'includes/services.resource_build'); + // Return resources representing legacy services + return _services_core_resources(); +} + +/** + * Implementation of hook_services_authentication_info(). + */ +function services_services_authentication_info() { + return array( + 'title' => t('Session authentication'), + 'description' => t("Uses Drupal's built in sessions to authenticate."), + 'authenticate_call' => '_services_sessions_authenticate_call', + ); +} + +/** + * Authenticates a call using Drupal's built in sessions + * + * @return string + * Error message in case error occured. + */ +function _services_sessions_authenticate_call($module, $controller) { + global $user; + $original_user = services_get_server_info('original_user'); + if ($original_user->uid == 0) { + return; + } + + if ($controller['callback'] != '_user_resource_get_token') { + $non_safe_method_called = !in_array($_SERVER['REQUEST_METHOD'], array('GET', 'HEAD', 'OPTIONS', 'TRACE')); + $csrf_token_invalid = !isset($_SERVER['HTTP_X_CSRF_TOKEN']) || !drupal_valid_token($_SERVER['HTTP_X_CSRF_TOKEN'], 'services'); + if ($non_safe_method_called && $csrf_token_invalid) { + return t('CSRF validation failed'); + } + } + + if ($user->uid != $original_user->uid) { + $user = $original_user; + } +} + +/** + * Get operation class information. + * + * @return array An array with operation class information keyed by operation machine name. + */ +function services_operation_class_info() { + return array( + 'operations' => array( + 'title' => t('CRUD operations'), + 'name' => t('CRUD operation'), + 'class_singular' => 'operation', + ), + 'actions' => array( + 'title' => t('Actions'), + 'name' => t('action'), + 'class_singular' => 'action', + ), + 'relationships' => array( + 'title' => t('Relationships'), + 'name' => t('relationship'), + 'class_singular' => 'relationship', + ), + 'targeted_actions' => array( + 'title' => t('Targeted actions'), + 'name' => t('targeted action'), + 'class_singular' => 'targeted_action', + ), + ); +} + +/** + * Returns all the controller names for a endpoint. + * + * @param string $endpoint + * The endpoint that should be used. + * @return array + * An array containing all controller names. + */ +function services_controllers_list($endpoint) { + $controllers = array(); + $class_info = services_operation_class_info(); + + $resources = services_get_resources($endpoint); + foreach ($resources as $resource_name => $resource) { + foreach ($class_info as $class_name => $class) { + if (empty($resource[$class_name])) { + continue; + } + foreach ($resource[$class_name] as $op_name => $op) { + $method = "{$resource_name}.{$op_name}"; + if (empty($controllers[$method])) { + $controllers[$method] = $method; + } + else { + watchdog('services', 'Naming collision when listing controllers as methods. The %class %operation is not included in the listing.', array( + '%class' => $class['name'], + '%operation' => $op_name, + ), WATCHDOG_WARNING); + } + } + } + } + return $controllers; +} + +/** + * Returns the requested controller. + * + * @param string $name + * The name of the controller in the format: {resource}.{name} or + * {resource}.{operation}. Examples: "node.retrieve", "system.getVariable". + * @param string $endpoint + * The endpoint that should be used. + */ +function services_controller_get($name, $endpoint) { + list($resource_name, $method) = explode('.', $name); + + $resources = services_get_resources($endpoint); + if (isset($resources[$resource_name])) { + $res = $resources[$resource_name]; + if (isset($res[$method])) { + return $res[$method]; + } + else { + $class_info = services_operation_class_info(); + // Handle extended operations + foreach ($class_info as $class => $info) { + if (isset($res[$class]) && isset($res[$class][$method])) { + return $res[$class][$method]; + } + } + } + } +} + +/** + * Returns an array of available updates versions for a resource. + * + * @return + * If services has updates, an array of available updates sorted by version. + * Otherwise, array(). + */ +function services_get_updates() { + $updates = &drupal_static(__FUNCTION__, array()); + if (!isset($updates) || empty($updates)) { + $updates = array(); + module_load_include('inc', 'services', 'includes/services.resource_build'); + // Load the resources for services. + _services_core_resources(); + // Prepare regular expression to match all possible defined + // _resource_resource_method_update_N_N(). + $regexp = '/_(?P.+)_resource_(?P.+)_update_(?P\d+)_(?P\d+)$/'; + $functions = get_defined_functions(); + // Narrow this down to functions ending with an integer, since all + // _resource_resource_method_update_N_N() functions end this way, and there are other + // possible functions which match '_update_'. We use preg_grep() here + // instead of foreaching through all defined functions, since the loop + // through all PHP functions can take significant page execution time. + // Luckily this only happens when the cache is cleared for an endpoint + // and resources are re-generated. + foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { + // If this function is a service update function, add it to the list of + // services updates. + if (preg_match($regexp, $function, $matches)) { + $resource = $matches['resource']; + $method = $matches['method']; + $major = $matches['major']; + $minor = $matches['minor']; + + $updates[$resource][$method][] = array( + 'version' => $major .'_'. $minor, + 'major' => $major, + 'minor' => $minor, + 'callback' => $function, + 'resource' => $resource, + 'method' => $method, + ); + } + } + } + return $updates; +} + +/** + * Determine if any potential versions exist as valid headers. + * returns false if no version is present in the header for the specific call. + */ +function _services_version_header_options() { + $available_headers = array(); + $updates = services_get_updates(); + if(is_array($updates)) { + foreach ($updates as $resource => $update) { + foreach ($update as $method_name => $method) { + $available_headers[] = 'services_'. $resource .'_'.$method_name .'_version'; + } + } + } + foreach($available_headers as $key => $version_header_option) { + if(array_key_exists($version_header_option, $headers = getallheaders())) { + $version = $headers[$version_header_option]; + } + } + return isset($version) ? $version : FALSE; +} + +/** + * Returns currently set api version for an endpoint resource method. + * + * @param $endpoint + * A fully loadded endpoint. + * @param $resource + * A resource name. + * @param $method + * A method name. + * @return + * an array with the major and minor api versions + */ +function services_get_resource_api_version($endpoint, $resource, $method) { + if (isset($endpoint->resources[$resource]) ) { + $class_info = services_operation_class_info(); + foreach ($class_info as $class_name => $class) { + if (!empty($resource[$class_name])) { + if (isset($endpoint->resources[$resource][$class_name][$method]['settings']['services']['resource_api_version'])) { + if($version = _services_version_header_options()) { + $split = explode('.', $version); + } else { + $split = explode('.', $endpoint->resources[$resource][$class_name][$method]['settings']['services']['resource_api_version']); + } + return array( + 'major' => $split[0], + 'minor' => $split[1], + ); + } + } + } + } +} + +/** + * Apply versions to the controller. + * + * @param $controller + * A controller array. + * @param $options + * A options array filled with verison information. + * @return + * An array with the major and minor api versions + */ +function services_request_apply_version(&$controller, $options = array()) { + if (isset($options)) { + extract($options); + } + if (isset($version) && $version == '1.0') { + //do nothing + return; + } + $updates = services_get_updates(); + if (isset($method) && isset($updates[$resource][$method])) { + foreach ($updates[$resource][$method] as $update) { + if (!isset($version)) { + $endpoint = services_get_server_info('endpoint', ''); + $endpoint = services_endpoint_load($endpoint); + $default_version = services_get_resource_api_version($endpoint, $resource, $method); + } + else { + $default_version = explode('.', $version); + $default_version['major'] = $default_version[0]; + $default_version['minor'] = $default_version[1]; + } + + // Apply updates until we hit our default update for the site. + if ($update['major'] <= $default_version['major'] && $update['minor'] <= $default_version['minor']) { + $update_data = call_user_func($update['callback']); + $controller = array_merge($controller, $update_data); + } + } + } +} + +/** + * Convert a resource to RPC-style methods. + * + * @param array $resource + * A resource definition. + * @param string $resource_name + * The resource name, ie: node. + * + * @return array + * An array of RPC method definitions + */ +function services_resources_as_procedures($resource, $resource_name) { + $methods = array(); + $class_info = services_operation_class_info(); + foreach ($class_info as $class_name => $class) { + if (empty($resource[$class_name])) { + continue; + } + foreach ($resource[$class_name] as $op_name => $op) { + $method_name = "{$resource_name}.{$op_name}"; + if (empty($methods[$method_name])) { + $methods[$method_name] = array( + 'method' => $method_name, + ) + $op; + } + else { + watchdog('services', 'Naming collision when listing controllers as methods. The %class %operation wont be available for RPC-style servers.', array( + '%class' => $class['name'], + '%operation' => $op_name, + ), WATCHDOG_WARNING); + } + } + } + return $methods; +} + +/** + * Helper function to build index queries. + * + * @param $query + * Object database query object. + * @param $page + * Integer page number we are requesting. + * @param $fields + * Array fields to return. + * @param $parameter + * Array parameters to add to the index query. + * @param $page_size + * Integer number of items to be returned. + * @param $resource + * String name of the resource building the index query + */ +function services_resource_build_index_query($query, $page, $fields, $parameters, $page_size, $resource) { + $default_limit = variable_get("services_{$resource}_index_page_size", 20); + if (!user_access('perform unlimited index queries') && $page_size > $default_limit) { + $page_size = $default_limit; + } + $query->range($page * $page_size, $page_size); + if ($fields == '*') { + $query->fields('t'); + } + else { + $query->fields('t', explode(',', $fields)); + } + if (isset($parameters) && is_array($parameters)) { + foreach ($parameters as $parameter => $parameter_value) { + $query->condition($parameter, services_str_getcsv($parameter_value), 'IN'); + } + } +} + + +/** + * Emulate str_getcsv on systems where it is not available. + * + * @ingroup php_wrappers + */ +function services_str_getcsv($input, $delimiter = ',', $enclosure = '"', $escape = '\\') { + $ret = array(); + + if (!function_exists('str_getcsv')) { + $temp = fopen("php://memory", "rw"); + fwrite($temp, $input); + fseek($temp, 0); + $ret = fgetcsv($temp, 0, $delimiter, $enclosure); + fclose($temp); + } + else { + $ret = str_getcsv($input, $delimiter, $enclosure, $escape); + } + return $ret; +} + + +/** + * Helper function to build a list of items satisfying the index query. + * + * @param $results + * Object database query results object. + * @param $type + * String type of index that is being processed. + * @param $field + * String field to use for looking up uri. + */ +function services_resource_build_index_list($results, $type, $field) { + // Put together array of matching items to return. + $items = array(); + foreach ($results as $result) { + if ($uri = services_resource_uri(array($type, $result->{$field}))) { + $result->uri = $uri; + if ($type == 'user') { + services_remove_user_data($result); + } + } + $items[] = $result; + } + + return $items; +} + +/** + * Helper function to remove data from the user object. + * + * @param $account + * Object user object. + */ +function services_remove_user_data(&$account) { + global $user; + + // Remove the user password from the account object. + unset($account->pass); + + // Remove the user mail, if current user don't have "administer users" + // permission, and the requested account not match the current user. + if (!user_access('administer users') && $account->uid !== $user->uid) { + unset($account->mail); + } + + // Remove the user init, if current user don't have "administer users" + // permission. + if (!user_access('administer users')) { + unset($account->init); + } + + drupal_alter('services_account_object', $account); + + // Add the full URL to the user picture, if one is present. + if (variable_get('user_pictures', FALSE) && isset($account->picture->uri)) { + $account->picture->url = file_create_url($account->picture->uri); + } +} + +/** + * Helper function to execute a index query. + * + * @param $query + * Object dbtng query object. + */ +function services_resource_execute_index_query($query) { + try { + return $query->execute(); + } + catch (PDOException $e) { + return services_error(t('Invalid query provided, double check that the fields and parameters you defined are correct and exist. ' . $e->getMessage()), 406); + } +} + +/** + * If we are running nginx we need to implement getallheaders our self. + * + * Code is taken from http://php.net/manual/en/function.getallheaders.php + */ +if (!function_exists('getallheaders')) { + function getallheaders() { + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + + return $headers; + } +} + +/** + * Page callback to generate token. + */ +function _services_session_token() { + drupal_add_http_header('Content-Type', 'text/plain'); + print drupal_get_token('services'); + drupal_exit(); +} diff --git a/openthess/modules/services/tests/functional/NoAuthEndpointTestRunner.test b/openthess/modules/services/tests/functional/NoAuthEndpointTestRunner.test new file mode 100644 index 0000000..f52a92b --- /dev/null +++ b/openthess/modules/services/tests/functional/NoAuthEndpointTestRunner.test @@ -0,0 +1,30 @@ + 'Services Endpoint tests, no auth', + 'description' => 'Test the endpoint functionality when no authentication is turned on', + 'group' => 'Services', + ); + } + +} \ No newline at end of file diff --git a/openthess/modules/services/tests/functional/ServicesAliasTests.test b/openthess/modules/services/tests/functional/ServicesAliasTests.test new file mode 100644 index 0000000..9fbbe71 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesAliasTests.test @@ -0,0 +1,104 @@ +populateEndpointFAPI() ; + $endpoint = new stdClass; + $endpoint->disabled = FALSE; + $endpoint->api_version = 3; + $endpoint->name = $edit['name']; + $endpoint->server = $edit['server']; + $endpoint->path = $edit['path']; + $endpoint->authentication = array( + 'services' => 'services', + ); + $endpoint->server_settings = array( + 'formatters' => array( + 'json' => TRUE, + 'bencode' => TRUE, + 'rss' => TRUE, + 'plist' => TRUE, + 'xmlplist' => TRUE, + 'php' => TRUE, + 'yaml' => TRUE, + 'jsonp' => FALSE, + 'xml' => FALSE, + ), + 'parsers' => array( + 'application/x-yaml' => TRUE, + 'application/json' => TRUE, + 'application/vnd.php.serialized' => TRUE, + 'application/plist' => TRUE, + 'application/plist+xml' => TRUE, + 'application/x-www-form-urlencoded' => TRUE, + 'multipart/form-data' => TRUE, + ), + ); + $endpoint->resources = array( + 'user' => array( + 'alias' => 'user-alias', + 'actions' => array( + 'login' => array( + 'enabled' => 1, + ), + ), + ), + ); + $endpoint->debug = 1; + $endpoint->export_type = FALSE; + services_endpoint_save($endpoint); + $endpoint = services_endpoint_load($endpoint->name); + $this->assertTrue($endpoint->name == $edit['name'], 'Endpoint successfully created'); + $this->endpoint = $endpoint; + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Alias', + 'description' => 'Test the Aliases functionality.', + 'group' => 'Services', + ); + } + + /** + * Testing parser functionality. + */ + public function testAlias() { + $account = $this->drupalCreateUser(); + + // Logout first. + $this->drupalLogout(); + + // Try to login using alias. + $response = $this->servicesPost($this->endpoint->path . '/user-alias/login', array('username' => $account->name, 'password' => $account->pass_raw)); + + $response_data = $response['body']; + + $proper_answer = isset($response_data->sessid) + && isset($response_data->user) + && $response_data->user->name == $account->name; + $this->assertTrue($proper_answer, 'User successfully logged in.', 'Alias: User login'); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesArgumentsTests.test b/openthess/modules/services/tests/functional/ServicesArgumentsTests.test new file mode 100644 index 0000000..6f8b6e5 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesArgumentsTests.test @@ -0,0 +1,153 @@ + 'Arguments handling', + 'description' => 'Test of arguments handling.', + 'group' => 'Services', + ); + } + + public function setUp() { + parent::setUp('ctools', 'services', 'services_test_resource'); + // Set up endpoint. + $this->endpoint = $this->saveNewEndpoint(); + } + + /** + * Test two path arguments of + */ + function testPathArguments() { + $arg1 = $this->randomName(); + $arg2 = $this->randomName(); + $arg3 = $this->randomName(); + $result = $this->servicesGet($this->endpoint->path . '/services_arguments_test/' . $arg1 . '/' . $arg2 . '/' . $arg3); + $this->assertEqual($result['body'], + format_string('Services arguments test @arg1 @arg2 @arg3', array('@arg1' => $arg1, '@arg2' => $arg2, '@arg3' => $arg3)), + 'Path arguments work properly.', 'Arguments'); + $result = $this->servicesGet($this->endpoint->path . '/services_arguments_test/' . $arg1 . '/' . $arg2); + $this->assertEqual($result['body'], + format_string('Services arguments test @arg1 @arg2 0', array('@arg1' => $arg1, '@arg2' => $arg2)), + 'Path arguments with default value work properly.', 'Arguments'); + $result = $this->servicesGet($this->endpoint->path . '/services_arguments_test/' . $arg1); + $this->assertEqual($result['status'], 'HTTP/1.1 404 Not found: Could not find the controller.', 'Error triggered when required argument is missing.', 'Arguments'); + } + + public function saveNewEndpoint() { + $edit = $this->populateEndpointFAPI() ; + $endpoint = new stdClass; + $endpoint->disabled = FALSE; /* Edit this to true to make a default endpoint disabled initially */ + $endpoint->api_version = 3; + $endpoint->name = $edit['name']; + $endpoint->title = $edit['title']; + $endpoint->server = $edit['server']; + $endpoint->path = $edit['path']; + $endpoint->authentication = array( + 'services' => 'services', + ); + $endpoint->server_settings = array( + 'formatters' => array( + 'json' => TRUE, + 'bencode' => TRUE, + 'rss' => TRUE, + 'plist' => TRUE, + 'xmlplist' => TRUE, + 'php' => TRUE, + 'yaml' => TRUE, + 'jsonp' => FALSE, + 'xml' => FALSE, + ), + 'parsers' => array( + 'application/x-yaml' => TRUE, + 'application/json' => TRUE, + 'application/vnd.php.serialized' => TRUE, + 'application/plist' => TRUE, + 'application/plist+xml' => TRUE, + 'application/x-www-form-urlencoded' => TRUE, + ), + ); + $endpoint->resources = array( + 'system' => array( + 'alias' => '', + 'actions' => array( + 'connect' => array( + 'enabled' => 1, + ), + 'get_variable' => array( + 'enabled' => 1, + ), + 'set_variable' => array( + 'enabled' => 1, + ), + ), + ), + 'user' => array( + 'alias' => '', + 'operations' => array( + 'create' => array( + 'enabled' => 1, + ), + 'retrieve' => array( + 'enabled' => 1, + ), + 'update' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'login' => array( + 'enabled' => 1, + ), + 'logout' => array( + 'enabled' => 1, + ), + ), + ), + 'services_arguments_test' => array( + 'alias' => '', + 'operations' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + ), + ), + ); + $endpoint->debug = 1; + $endpoint->export_type = FALSE; + services_endpoint_save($endpoint); + $endpoint = services_endpoint_load($endpoint->name); + $this->assertTrue($endpoint->name == $edit['name'], 'Endpoint successfully created'); + return $endpoint; + } + + public function populateEndpointFAPI() { + return array( + 'name' => 'machinename', + 'title' => $this->randomName(20), + 'path' => $this->randomName(10), + 'server' => 'rest_server', + ); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesEndpointTests.test b/openthess/modules/services/tests/functional/ServicesEndpointTests.test new file mode 100644 index 0000000..eca5883 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesEndpointTests.test @@ -0,0 +1,86 @@ +populateEndpointFAPI(); +// Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer site configuration', + )); + $this->drupalLogin($this->privilegedUser); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->drupalGet($edit['path']); + } + + /** + * Test adding an endpoint succeeds. + */ + public function testSuccessfulAddEndpoint() { + $edit = $this->populateEndpointFAPI(); + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer content types', + 'administer site configuration', + )); + $this->drupalLogin($this->privilegedUser); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->assertResponse('200', 'expected 200'); + $this->drupalGet('admin/structure/services'); + $this->assertResponse('200', 'expected 200'); + + $this->assertText($edit['name'], 'Endpoint path appears'); + $this->assertText('Normal', 'Storage is in database'); + } + + /** + * Test missing path to endpoint causes an error. + */ + public function testMissingPath() { + $edit = $this->populateEndpointFAPI(); + unset($edit['path']); + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer content types', + 'administer site configuration', + )); + $this->drupalLogin($this->privilegedUser); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->assertResponse('200', 'expected 200'); + + $this->assertText('Path to endpoint field is required.', 'Endpoint path missing error message.'); + $this->assertFieldByName('server', 'rest_server', 'Server is rest server'); + } + /** + * Test missing server for endpoint causes an error. + */ + public function testMissingServer() { + $edit = $this->populateEndpointFAPI(); + unset($edit['server']); + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer content types', + 'administer site configuration', + )); + $this->drupalLogin($this->privilegedUser); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->assertResponse('200', 'expected 200'); + + $this->assertText('Server field is required.', 'Server missing error message.'); + $this->assertFieldByName('name', $edit['name'], 'Name field remains.'); + } +} \ No newline at end of file diff --git a/openthess/modules/services/tests/functional/ServicesParserTests.test b/openthess/modules/services/tests/functional/ServicesParserTests.test new file mode 100644 index 0000000..d123426 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesParserTests.test @@ -0,0 +1,119 @@ +populateEndpointFAPI() ; + $endpoint = new stdClass; + $endpoint->disabled = FALSE; + $endpoint->api_version = 3; + $endpoint->name = $edit['name']; + $endpoint->server = $edit['server']; + $endpoint->path = $edit['path']; + $endpoint->authentication = array( + 'services' => 'services', + ); + $endpoint->server_settings = array( + 'formatters' => array( + 'php' => TRUE, + ), + 'parsers' => array( + 'application/x-yaml' => TRUE, + 'application/json' => TRUE, + 'application/vnd.php.serialized' => TRUE, + 'application/plist' => TRUE, + 'application/plist+xml' => TRUE, + 'application/x-www-form-urlencoded' => FALSE, + ), + ); + $endpoint->resources = array( + 'user' => array( + 'actions' => array( + 'login' => array( + 'enabled' => 1, + ), + 'logout' => array( + 'enabled' => 1, + ), + ), + ), + ); + $endpoint->debug = 1; + $endpoint->export_type = FALSE; + services_endpoint_save($endpoint); + $endpoint = services_endpoint_load($endpoint->name); + $this->assertTrue($endpoint->name == $edit['name'], 'Endpoint successfully created'); + $this->endpoint = $endpoint; + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Parser', + 'description' => 'Test the Parser functionality.', + 'group' => 'Services', + ); + } + + /** + * Testing parser functionality. + */ + public function testParser() { + $account = $this->drupalCreateUser(); + + // Logout first. + $this->drupalLogout(); + + // Try to login. By default servicesPost uses + // 'application/x-www-form-urlencoded' type. So it should be refused. + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $account->pass_raw)); + + $this->assertTrue(strpos($response['header'], '406 Not Acceptable: Unsupported request content type application/x-www-form-urlencoded') !== FALSE, + 'Do not accept application/x-www-form-urlencoded if disabled.', 'Parser'); + } + + /** + * Do JSON call. Ensure it is parsed properly. + */ + public function testJSONCall() { + $account = $this->drupalCreateUser(); + + // Logout first. + $this->drupalLogout(); + + // Do JSON call to login. + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $account->pass_raw), array(), 'json'); + + $body = $response['body']; + + $proper_answer = isset($body->sessid) + && isset($body->user) + && $body->user->name == $account->name; + $this->assertTrue($proper_answer, 'User successfully logged in via JSON call.', 'JSON Call: Login'); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesResourceCommentTests.test b/openthess/modules/services/tests/functional/ServicesResourceCommentTests.test new file mode 100644 index 0000000..1cea6b5 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesResourceCommentTests.test @@ -0,0 +1,364 @@ +endpoint = $this->saveNewEndpoint(); + + // Create and log in our privileged user. + $this->privileged_user = $this->drupalCreateUser(); + $this->drupalLogin($this->privileged_user); + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Resource Comment', + 'description' => 'Test the resource Comment methods and actions.', + 'group' => 'Services', + ); + } + public function testCommentIndex() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer comments', + )); + $this->drupalLogin($this->privilegedUser); + + + // Create a set of comments. The comment resource returns 20 comments at a time, + // so we create two pages and a half worth. + $comments = array(); + $count = 50; + $node = $this->drupalCreateNode(); + $nid = $node->nid; + for ($i = 0; $i < $count; $i++) { + $comment = (object)$this->getCommentValues($nid); + $comment->created = REQUEST_TIME + $i; + comment_save($comment); + $comments[$comment->cid] = $comment; + } + + // Get the content. + $page_count = ceil(count($comments) / 20); + $retrieved_comments = array(); + for ($page = 0; $page < $page_count; $page++) { + $responseArray = $this->servicesGet($this->endpoint->path . '/comment', array('page' => $page, 'fields' => 'cid,subject')); + $this->assertTrue(count($responseArray['body']) <= 20, 'Correct number of items returned'); + + // Store the returned comment IDs. + foreach ($responseArray['body'] as $comment) { + if (isset($retrieved_comments[$comment->cid])) { + $this->fail(format_string('Duplicate comment @cid returned.', array('@cid' => $comment->cid))); + } + $retrieved_comments[$comment->cid] = TRUE; + + $this->assertTrue($comments[$comment->cid]->subject == $comment->subject, 'Successfully received Comment info', 'CommentResource: Index'); + } + } + // We should have got all the comments. + $expected_cids = array_keys($comments); + sort($expected_cids); + $retrieved_cids = array_keys($retrieved_comments); + sort($retrieved_cids); + + $this->assertEqual($expected_cids, $retrieved_cids, 'Retrieved all comments'); + + // The n+1 page should be empty. + $responseArray = $this->servicesGet($this->endpoint->path . '/comment', array('page' => $page_count + 1)); + $this->assertEqual(count($responseArray['body']), 0, 'The n+1 page is empty'); + } + + /** + * Test create comment. + */ + public function testCommentCreate() { + $node = $this->drupalCreateNode(); + + // Create comment. + $comment = $this->getCommentValues($node->nid); + + $response_array = $this->servicesPost($this->endpoint->path . '/comment', $comment); + $commentResourceCreateReturn = $response_array['body']; + $this->assertTrue(isset($commentResourceCreateReturn['cid']), + 'Comment was successfully created', 'CommentResource: Create'); + + // Assert subject and body of comment are the same as we created. + $newComment = comment_load($commentResourceCreateReturn['cid']); + $this->assertTrue($newComment->subject == $comment['subject'], 'Subject was the same', 'CommentResource: Create'); + $this->assertTrue($newComment->comment_body[LANGUAGE_NONE][0]['value'] == $comment['comment_body'][LANGUAGE_NONE][0]['value'], + 'Body was the same', 'CommentResource: Create'); + + // Try to create comment with full_html filter that is disabled by default. + $comment = array( + 'subject' => $this->randomString(), + 'comment_body' => array( + LANGUAGE_NONE => array( + array( + 'value' => $this->randomString(), + 'format' => 'full_html', + ) + ) + ), + 'name' => $this->privileged_user->name, + 'language' => LANGUAGE_NONE, + 'nid' => $node->nid, + 'uid' => $this->privileged_user->uid, + 'cid' => NULL, + 'pid' => 0, + ); + $response_array = $this->servicesPost($this->endpoint->path . '/comment', $comment); + + $this->assertTrue(strpos($response_array['status'], 'An illegal choice has been detected.'), + 'User cannot post comment with full_html filter chosen.', 'CommentResource: Create'); + } + + /** + * Test create comment (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + public function testCommentCreateLegacy() { + $node = $this->drupalCreateNode(); + + // Create comment. + $comment = $this->getCommentValues($node->nid); + + $response_array = $this->servicesPost($this->endpoint->path . '/comment', array('comment' => $comment)); + $commentResourceCreateReturn = $response_array['body']; + $this->assertTrue(isset($commentResourceCreateReturn['cid']), + 'Comment was successfully created', 'CommentResource: Create (Legacy)'); + + // Assert subject and body of comment are the same as we created. + $newComment = comment_load($commentResourceCreateReturn['cid']); + $this->assertTrue($newComment->subject == $comment['subject'], + 'Subject was the same', 'CommentResource: Create (Legacy)'); + $this->assertTrue($newComment->comment_body[LANGUAGE_NONE][0]['value'] == $comment['comment_body'][LANGUAGE_NONE][0]['value'], + 'Body was the same', 'CommentResource: Create (Legacy)'); + + // Try to create comment with full_html filter that is disabled by default. + $comment = array( + 'subject' => $this->randomString(), + 'comment_body' => array( + LANGUAGE_NONE => array( + array( + 'value' => $this->randomString(), + 'format' => 'full_html', + ) + ) + ), + 'name' => $this->privileged_user->name, + 'language' => LANGUAGE_NONE, + 'nid' => $node->nid, + 'uid' => $this->privileged_user->uid, + 'cid' => NULL, + 'pid' => 0, + ); + $response_array = $this->servicesPost($this->endpoint->path . '/comment', array('comment' => $comment)); + + $this->assertTrue(strpos($response_array['status'], 'An illegal choice has been detected.'), + 'User cannot post comment with full_html filter chosen.', 'CommentResource: Create (Legacy)'); + } + + /** + * Test retrieve method. + */ + function testCommentRetrieve() { + $path = $this->endpoint->path; + + // Create node. + $node = $this->drupalCreateNode(); + + $comment_args = $this->getCommentValues($node->nid); + + $comment = (object)$comment_args; + + comment_save($comment); + $comment_args['cid'] = $comment->cid; + + $response = $this->servicesGet($path . '/comment/' . $comment->cid); + + $comment_retrieve = (array)$response['body']; + + $comment_intersect = array_intersect_assoc($comment_retrieve, $comment_args); + + // Unset save_value as we don't have this key in arguments. + unset($comment_intersect['comment_body'][LANGUAGE_NONE][0]['safe_value']); + + $this->assertEqual($comment_args, $comment_intersect, 'Comment retrieved properly.', 'CommentResource: Retrieve'); + } + + /** + * Test update method. + */ + function testCommentUpdate() { + $path = $this->endpoint->path; + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer comments', + )); + $this->drupalLogin($this->privilegedUser); + // Create node. + $node = $this->drupalCreateNode(); + + $comment_args = $this->getCommentValues($node->nid); + + $comment = (object)$comment_args; + + comment_save($comment); + $cid = $comment->cid; + $comment_args['cid'] = $cid; + + $comment_update = $comment_args; + $comment_update['subject'] = $this->randomString(); + $comment_update['comment_body'][LANGUAGE_NONE][0]['value'] = $this->randomString(); + + $response = $this->servicesPut($path . '/comment/' . $cid, $comment_update); + + $comment_load = (array)comment_load($cid); + + $comment_intersect = array_intersect_assoc($comment_load, $comment_update); + + // Unset save_value as we don't have this key in arguments. + unset($comment_intersect['comment_body'][LANGUAGE_NONE][0]['safe_value']); + + $this->assertEqual($comment_update, $comment_intersect, 'Comment updated properly.', 'CommentResource: Update'); + } + + /** + * Test update method. + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testCommentUpdateLegacy() { + $path = $this->endpoint->path; + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer comments', + )); + $this->drupalLogin($this->privilegedUser); + // Create node. + $node = $this->drupalCreateNode(); + + $comment_args = $this->getCommentValues($node->nid); + + $comment = (object)$comment_args; + + comment_save($comment); + $cid = $comment->cid; + $comment_args['cid'] = $cid; + + $comment_update = $comment_args; + $comment_update['subject'] = $this->randomString(); + $comment_update['comment_body'][LANGUAGE_NONE][0]['value'] = $this->randomString(); + + $response = $this->servicesPut($path . '/comment/' . $cid, array('data' => $comment_update)); + + $comment_load = (array)comment_load($cid); + + $comment_intersect = array_intersect_assoc($comment_load, $comment_update); + + // Unset save_value as we don't have this key in arguments. + unset($comment_intersect['comment_body'][LANGUAGE_NONE][0]['safe_value']); + + $this->assertEqual($comment_update, $comment_intersect, 'Comment updated properly.', 'CommentResource: Update'); + } + + /** + * Test delete method. + */ + function testCommentDelete() { + $path = $this->endpoint->path; + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'administer comments', + )); + $this->drupalLogin($this->privilegedUser); + // Create node with commenting. + $node = $this->drupalCreateNode(); + + $comment_args = $this->getCommentValues($node->nid); + + $comment = (object)$comment_args; + + comment_save($comment); + $cid = $comment->cid; + $comment_args['cid'] = $cid; + + $response = $this->servicesDelete($path . '/comment/' . $cid); + + $comment_load = comment_load($cid); + + $this->assertTrue(empty($comment_load), 'Comment deleted properly.', 'CommentResource: Delete'); + } + + + /** + * Test countAll method. + */ + function testCommentCountAll() { + $path = $this->endpoint->path; + // Generate comments. + $settings = array('comment' => 1); + $node = $this->drupalCreateNode($settings); + for ($i = 0; $i < 5; $i++) { + $comment = (object)$this->getCommentValues($node->nid); + comment_save($comment); + } + + $response = $this->servicesPost($path . '/comment/countAll', array('nid' => $node->nid)); + $this->assertEqual($response['body'], 5, 'Counted number of comments properly.', 'CommentResource: countAll'); + } + + /** + * Test countNew method. + */ + function testCommentCountNew() { + $path = $this->endpoint->path; + // Generate comments. + $node = $this->drupalCreateNode(); + $nid = $node->nid; + for ($i = 0; $i < 5; $i++) { + $comment = (object)$this->getCommentValues($nid); + $comment->created = REQUEST_TIME + $i; + comment_save($comment); + $comments[] = comment_load($comment->cid); + } + + $response = $this->servicesPost($path . '/comment/countNew', array('nid' => $node->nid)); + $this->assertEqual($response['body'], 5, 'Received number of all new comments.', 'CommentResource: countNew'); + + $since = $comments[2]->created; + + $response = $this->servicesPost($path . '/comment/countNew', array('nid' => $node->nid, 'since' => $since)); + $this->assertEqual($response['body'], 2, 'Received number of new comments.', 'CommentResource: countNew'); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesResourceDisabledTests.test b/openthess/modules/services/tests/functional/ServicesResourceDisabledTests.test new file mode 100644 index 0000000..19c50b1 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesResourceDisabledTests.test @@ -0,0 +1,97 @@ + 'Services Disabled Resource Test', + 'description' => 'Assert that a resource is disabled when a request is made to it.', + 'group' => 'Services', + ); + } + + /** + * Implementation of setUp(). + */ + public function setUp() { + parent::setUp( + 'ctools', + 'services', + 'rest_server' + ); + // Set up endpoint. + $this->endpoint = $this->saveNewEndpoint(); + // Set up privileged user and login. + $this->privileged_user = $this->drupalCreateUser(array('get a system variable', 'set a system variable')); + $this->drupalLogin($this->privileged_user); + } + + /** + * Save a new endpoint without any resources enabled. This is a method from + * ServicesWebTestCase that has been modified. + */ + public function saveNewEndpoint() { + $edit = $this->populateEndpointFAPI() ; + $endpoint = new stdClass; + $endpoint->disabled = FALSE; /* Edit this to true to make a default endpoint disabled initially */ + $endpoint->api_version = 3; + $endpoint->name = $edit['name']; + $endpoint->server = $edit['server']; + $endpoint->path = $edit['path']; + $endpoint->authentication = array( + 'services' => 'services', + ); + $endpoint->server_settings = array( + 'formatters' => array( + 'json' => TRUE, + 'bencode' => TRUE, + 'rss' => TRUE, + 'plist' => TRUE, + 'xmlplist' => TRUE, + 'php' => TRUE, + 'yaml' => TRUE, + 'jsonp' => FALSE, + 'xml' => FALSE, + ), + 'parsers' => array( + 'application/x-yaml' => TRUE, + 'application/json' => TRUE, + 'application/vnd.php.serialized' => TRUE, + 'application/plist' => TRUE, + 'application/plist+xml' => TRUE, + 'application/x-www-form-urlencoded' => TRUE, + 'multipart/form-data' => TRUE, + ), + ); + $endpoint->debug = 1; + $endpoint->export_type = FALSE; + services_endpoint_save($endpoint); + $endpoint = services_endpoint_load($endpoint->name); + $this->assertTrue($endpoint->name == $edit['name'], 'Endpoint successfully created'); + return $endpoint; + } + + /** + * Assert resource is disabled. + */ + function testResourceDisabled() { + $path = $this->endpoint->path; + // Call as authenticated user. + $response = $this->servicesPost($path . '/system/connect'); + $this->assertEqual($response['code'], 404, format_string('Services returned not found response code for disabled resource: %code.', array('%code' => $response['code']))); + $this->drupalLogout(); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesResourceFileTests.test b/openthess/modules/services/tests/functional/ServicesResourceFileTests.test new file mode 100644 index 0000000..7b13b14 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesResourceFileTests.test @@ -0,0 +1,263 @@ +endpoint = $this->saveNewEndpoint(); + // Create and log in our privileged user. + $this->privileged_user = $this->drupalCreateUser(array( + 'get any binary files', + 'save file information', + 'administer services', + 'administer site configuration', + 'bypass node access', + )); + $this->drupalLogin($this->privileged_user); + // Get a test file. + $this->testfiles = $this->drupalGetTestFiles('image'); + $this->testfile = current($this->testfiles); + } + + /** + * Implements getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Resource File', + 'description' => 'Test the resource File methods.', + 'group' => 'Services', + ); + } + + + public function testIndexFiles() { + // Create a set of files to test with + $files = array(); + foreach($this->testfiles as $file) { + file_save($file); + $files[$file->fid] = $file; + } + // Get the content. + $page_count = ceil(count($files) / 20); + $retrieved_files = array(); + for ($page = 0; $page < $page_count; $page++) { + $responseArray = $this->servicesGet($this->endpoint->path . '/file', array('page' => $page, 'fields' => 'fid,filename')); + $this->assertTrue(count($responseArray['body']) <= 20, 'Correct number of items returned'); + + // Store the returned file IDs. + foreach ($responseArray['body'] as $file) { + if (isset($retrieved_files[$file->fid])) { + $this->fail(format_string('Duplicate files @fid returned.', array('@fid' => $file->fid))); + } + $retrieved_files[$file->fid] = TRUE; + + $this->assertTrue($files[$file->fid]->filename == $file->filename, + 'Successfully received File info', 'FileResource: Index'); + } + } + // We should have got all the files. + $expected_fids = array_keys($files); + sort($expected_fids); + $retrieved_fids = array_keys($retrieved_files); + sort($retrieved_fids); + + $this->assertEqual($expected_fids, $retrieved_fids, 'Retrieved all files'); + + // The n+1 page should be empty. + $responseArray = $this->servicesGet($this->endpoint->path . '/file', array('page' => $page_count + 1)); + $this->assertEqual(count($responseArray['body']), 0, 'The n+1 page is empty'); + } + + /** + * Test create method. + */ + public function testResourceFileCreate() { + // Create file argument with data. + $filepath = file_default_scheme() . '://' . rand() . '/' . rand() . '/' . $this->testfile->filename; + $file = array( + 'filesize' => filesize($this->testfile->uri), + 'filename' => $this->testfile->filename, + 'filepath' => $filepath, + 'file' => base64_encode(file_get_contents($this->testfile->uri)), + 'uid' => $this->privileged_user->uid, + ); + + // Create file with call. + $result = $this->servicesPost($this->endpoint->path . '/file', $file); + $this->assertEqual($result['code'], 200, 'File created.', 'FileResource: Create'); + + // Load file and assert that it exists. + $file_load = file_load($result['body']['fid']); + $this->assertTrue(is_file($file_load->uri), 'New file saved to disk.', 'FileResource: Create'); + $this->assertEqual($file_load->uri, $filepath, + 'The path of newly created file placed into directory with random name.', 'FileResource: Create'); + } + /** + * Test create_raw method. + */ + public function testResourceFileCreateRaw() { + // Create file with call. + $result = $this->servicesPostFile($this->endpoint->path . '/file/create_raw', $this->testfile->uri); + $this->assertEqual($result['code'], 200, 'File created.', 'FileResource: Create'); + + // Load file and assert that it exists. + $file_load = file_load($result['body'][0]['fid']); + $this->assertTrue(is_file($file_load->uri), 'New file saved to disk.', 'FileResource: Create'); + } + /** + * Test create method (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + public function testResourceFileCreateLegacy() { + // Create file argument with data. + $file = array( + 'filesize' => filesize($this->testfile->uri), + 'filename' => $this->testfile->filename, + 'file' => base64_encode(file_get_contents($this->testfile->uri)), + 'uid' => $this->privileged_user->uid, + ); + + // Create file with call. + $result = $this->servicesPost($this->endpoint->path . '/file', array('file' => $file)); + $this->assertEqual($result['code'], 200, 'File created.', 'FileResource: Create (Legacy)'); + + // Load file and assert that it exists. + $file_load = file_load($result['body']['fid']); + $this->assertTrue(is_file($file_load->uri), 'New file saved to disk.', 'FileResource: Create (Legacy)'); + } + + /** + * Test retrieve method. + */ + public function testResourceFileRetrieve() { + $testfile = $this->testfile; + + $testfile->fid = NULL; + $testfile->uid = $this->privileged_user->uid; + file_save($testfile); + + // Retrieve file. + $result = $this->servicesGet($this->endpoint->path . '/file/' . $testfile->fid); + $file = $result['body']; + + // Assert that fid, filesize and uri are the same. + $this->assertTrue($file->fid == $testfile->fid + && $file->filesize == $testfile->filesize + && $file->uri == $testfile->uri, + 'File retrieved.', 'FileResource: Retrieve'); + } + + /** + * Test delete method. + */ + public function testResourceFileDelete() { + $testfile = $this->testfile; + + $testfile->fid = NULL; + $testfile->uid = $this->privileged_user->uid; + file_save($testfile); + + // Delete file via call. + $result = $this->servicesDelete($this->endpoint->path . '/file/' . $testfile->fid); + + // Try to load file. + $file_load = file_load($testfile->fid); + $this->assertTrue(empty($file_load), 'File deleted.', 'FileResource: Delete'); + } + + /** + * Attach file to the node. + */ + public function testCreateNodeWithFile() { + $filepath = file_default_scheme() . '://' . rand() . '/' . rand() . '/' . $this->testfile->filename; + + // Create file that managed by services. + $file = array( + 'filesize' => filesize($this->testfile->uri), + 'filename' => $this->testfile->filename, + 'filepath' => $filepath, + 'file' => base64_encode(file_get_contents($this->testfile->uri)), + 'uid' => $this->privileged_user->uid, + ); + + // Create file with call. + $result = $this->servicesPost($this->endpoint->path . '/file', $file); + + $fid = $result['body']['fid']; + $file_load = file_load($fid); + + // Try to delete the file and ensure that it is not possible. + $file_delete_result = file_delete($file_load); + $this->assertTrue($file_delete_result !== TRUE, 'It is not possible to delete file managed by services using file_delete().'); + + + // Create file that is not managed by services. + $file = array( + 'filesize' => filesize($this->testfile->uri), + 'filename' => $this->testfile->filename, + 'filepath' => $filepath, + 'file' => base64_encode(file_get_contents($this->testfile->uri)), + 'uid' => $this->privileged_user->uid, + 'status' => 0, + ); + + // Create file with call. + $result = $this->servicesPost($this->endpoint->path . '/file', $file); + + $fid = $result['body']['fid']; + $file_load = file_load($fid); + + // Create a node with this file attached. + $node = array( + 'title' => $this->randomString(), + 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomString()))), + 'type' => 'article', + 'name' => $this->privileged_user->name, + 'language' => LANGUAGE_NONE, + 'field_image' => array(LANGUAGE_NONE => array(array('fid' => $fid, 'display' => '1'))), + ); + $response_array = $this->servicesPost($this->endpoint->path . '/node', $node); + $nid = $response_array['body']['nid']; + + $node_load = node_load($nid, NULL, TRUE); + + $this->assertEqual($fid, $node_load->field_image[LANGUAGE_NONE][0]['fid'], 'File added to the node successfully.'); + + // Now file should be managed by node. Lets try to delete it and ensure + // that it is not possible. + $file_delete_result = file_delete($file_load); + $this->assertTrue($file_delete_result !== TRUE, 'It is not possible to delete file managed by node using file_delete().'); + + // Delete the node and assert that file can be deleted. + node_delete($nid); + + $file_delete_result = file_delete($file_load); + $this->assertTrue($file_delete_result === TRUE, 'File can be deleted after node has been removed.'); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesResourceNodeTests.test b/openthess/modules/services/tests/functional/ServicesResourceNodeTests.test new file mode 100644 index 0000000..20df5a9 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesResourceNodeTests.test @@ -0,0 +1,615 @@ +endpoint = $this->saveNewEndpoint(); + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Resource Node', + 'description' => 'Test the resource Node methods and actions.', + 'group' => 'Services', + ); + } + + /** + * testing node_resource Index + */ + public function testNewEndpointResourceNodeIndex() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'perform unlimited index queries', + )); + $this->drupalLogin($this->privilegedUser); + + // Create a set of nodes. The node resource returns 20 returns at a time, + // so we create two pages and a half worth. + $nodes = array(); + $count = 50; + for ($i = 0; $i < $count; $i++) { + $node = $this->drupalCreateNode(); + $nodes[$node->nid] = $node; + } + + // Get the content. + $page_count = ceil(count($nodes) / 20); + $retrieved_nodes = array(); + for ($page = 0; $page < $page_count; $page++) { + $responseArray = $this->servicesGet($this->endpoint->path . '/node', array('page' => $page, 'fields' => 'nid,title')); + $this->assertTrue(count($responseArray['body']) <= 20, 'Correct number of items returned'); + + // Store the returned node IDs. + foreach ($responseArray['body'] as $node) { + if (isset($retrieved_nodes[$node->nid])) { + $this->fail(format_string('Duplicate node @nid returned.', array('@nid' => $node->nid))); + } + $retrieved_nodes[$node->nid] = TRUE; + + $this->assertTrue($nodes[$node->nid]->title == $node->title, 'Successfully received Node info', 'NodeResource: Index'); + } + } + + // We should have got all the nodes. + $expected_nids = array_keys($nodes); + sort($expected_nids); + $retrieved_nids = array_keys($retrieved_nodes); + sort($retrieved_nids); + $this->assertEqual($expected_nids, $retrieved_nids, 'Retrieved all nodes'); + + // The n+1 page should be empty. + $responseArray = $this->servicesGet($this->endpoint->path . '/node', array('page' => $page_count + 1)); + $this->assertEqual(count($responseArray['body']), 0, 'The n+1 page is empty'); + + // Adjust the pager size. + $responseArray = $this->servicesGet($this->endpoint->path . '/node', array('fields' => 'nid,title', 'pagesize' => 40)); + $this->assertTrue(count($responseArray['body']) == 40, 'Correct number of items returned'); + + // Swap to user that can only use the default pager size. + $this->lessPrivilegedUser = $this->drupalCreateUser(array( + 'administer services', + )); + $this->drupalLogin($this->lessPrivilegedUser); + $responseArray = $this->servicesGet($this->endpoint->path . '/node', array('fields' => 'nid,title', 'pagesize' => 40)); + $this->assertTrue(count($responseArray['body']) == 20, 'Correct number of items returned'); + } + + /** + * testing node_resource Get + */ + public function testNewEndpointResourceNodeGet() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + )); + $this->drupalLogin($this->privilegedUser); + $node = $this->drupalCreateNode(); + $responseArray = $this->servicesGet($this->endpoint->path . '/node/' . $node->nid); + $this->assertTrue($node->title == $responseArray['body']->title, + 'Successfully received Node info', 'NodeResource: Retrieve'); + //Verify node not found. + unset($node); + $responseArray = $this->servicesGet($this->endpoint->path . '/node/99'); + $this->assertTrue($responseArray['code'] == '404', 'Successfully was rejected to non existent node', 'NodeResource: Retrieve'); + } + + /** + * Test loadNodeComments method. + */ + function testCommentLoadNodeComments() { + $path = $this->endpoint->path; + $this->privileged_user = $this->drupalCreateUser(); + $this->drupalLogin($this->privileged_user); + + // Create node with commenting. + $node = $this->drupalCreateNode(); + $nid = $node->nid; + + // Generate 15 comments for node. + $comments = array(); + for ($i = 1; $i <= 15; $i++) { + $comment = (object)$this->getCommentValues($nid); + comment_save($comment); + $comments[$i] = comment_load($comment->cid); + } + + // Generate some comments for another node. + $node2 = $this->drupalCreateNode(); + for ($i = 1; $i <= 5; $i++) { + $comment = (object)$this->getCommentValues($node2->nid); + comment_save($comment); + } + + // Load all comments of the first node. + $response = $this->servicesGet($path . '/node/'. $nid .'/comments'); + $this->assertEqual($comments, $response['body'], 'Received all 15 comments.', 'NodeResource: comments'); + + // Load only 5 comments of the first node. + $response = $this->servicesGet($path . '/node/'. $nid .'/comments', array('count' => 5)); + $this->assertEqual(array_slice($comments, 0, 5), array_slice($response['body'], 0, 5), 'Received last 5 comments.', 'NodeResource: comments'); + // Load only 5 comments of the first node starting from fifth comment. + $response = $this->servicesGet($path . '/node/'. $nid .'/comments', array('count' => 5, 'offset' => 5)); + $this->assertEqual(array_slice($comments, 5, 5), array_merge(array(),$response['body']), + 'Received 5 comments starting from fifth comment.', 'NodeResource: comments'); + } + + /** + * Testing node_resource Create. + */ + public function testEndpointResourceNodeCreate() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'bypass node access', + )); + $this->drupalLogin($this->privilegedUser); + $node = array( + 'title' => 'testing', + 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomString()))), + 'type' => 'page', + 'name' => $this->privilegedUser->name, + 'language' => LANGUAGE_NONE, + ); + + $responseArray = $this->servicesPost($this->endpoint->path . '/node', $node); + $nodeResourceCreateReturn = $responseArray['body']; + + $this->assertTrue(isset($nodeResourceCreateReturn['nid']), 'Node was successfully created', 'NodeResource: Create'); + $newNode = node_load($nodeResourceCreateReturn['nid']); + $this->assertTrue($newNode->title = $node['title'], 'Title was the same', 'NodeResource: Create'); + $this->assertTrue($newNode->body = $node['body'], 'Body was the same', 'NodeResource: Create'); + } + + /** + * Testing node_resource Create (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + public function testEndpointResourceNodeCreateLegacy() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'bypass node access', + )); + $this->drupalLogin($this->privilegedUser); + $node = array( + 'title' => 'testing', + 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomString()))), + 'type' => 'page', + 'name' => $this->privilegedUser->name, + 'language' => LANGUAGE_NONE, + ); + + $responseArray = $this->servicesPost($this->endpoint->path . '/node', array('node' => $node)); + $nodeResourceCreateReturn = $responseArray['body']; + + $this->assertTrue(isset($nodeResourceCreateReturn['nid']), 'Node was successfully created', 'NodeResource: Create (Legacy)'); + $newNode = node_load($nodeResourceCreateReturn['nid']); + $this->assertTrue($newNode->title = $node['title'], 'Title was the same', 'NodeResource: Create (Legacy)'); + $this->assertTrue($newNode->body = $node['body'], 'Body was the same', 'NodeResource: Create (Legacy)'); + } + + /** + * testing node_resource Created make ure it fails with no perms + */ + public function testEndpointResourceNodeCreateFail() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + )); + $this->drupalLogin($this->privilegedUser); + $node = array( + 'title' => 'testing', + 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomString()))), + 'type' => 'page', + 'name' => $this->privilegedUser->name, + 'language' => LANGUAGE_NONE, + ); + + $responseArray = $this->servicesPost($this->endpoint->path . '/node', array('node' => $node)); + + $this->assertTrue($responseArray['code'] == 403, 'User with not sufficient permissions cannot create node', 'NodeResource: Create'); + } + + /** + * testing node_resource Validate missing Title + */ + public function testEndpointResourceNodeCreateMissingTitle() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'bypass node access', + )); + $this->drupalLogin($this->privilegedUser); + + $node = array( + 'title' => '', + 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomString()))), + 'type' => 'page', + 'name' => $this->privilegedUser->name, + 'language' => LANGUAGE_NONE, + ); + + $responseArray = $this->servicesPost($this->endpoint->path . '/node', array('node' => $node)); + + $nodeResourceUpdateReturn = $responseArray['body']; + $this->assertTrue(strpos($responseArray['status'], 'Title field is required.'), + 'Node was not created without title.', 'NodeResource: Create'); + } + + /** + * Testing targeted_action attach_file. + */ + public function testAttachFileTargetedAction() { + // We will do test on the article node type. + // Create and log in our privileged user. + $account = $this->drupalCreateUser(array( + 'bypass node access', + )); + $this->drupalLogin($account); + + // Create article node. + $settings = array('type' => 'article'); + $node = $this->drupalCreateNode($settings); + + // Get a test file. + $testfiles = $this->drupalGetTestFiles('image'); + $testfile1 = array_pop($testfiles); + $testfile2 = array_pop($testfiles); + + // Attach one file. + $result = $this->servicesPostFile($this->endpoint->path . '/node/' . $node->nid . '/attach_file', + array($testfile1->uri), array(), array('field_name' => 'field_image')); + $node = node_load($node->nid, TRUE); + $this->assertEqual($testfile1->filename, $node->field_image[LANGUAGE_NONE][0]['filename'], 'One file has been attached.'); + + // Replace the file on the article node. + $result = $this->servicesPostFile($this->endpoint->path . '/node/' . $node->nid . '/attach_file', + array($testfile2->uri), array(), array('field_name' => 'field_image', 'attach' => FALSE)); + $node = node_load($node->nid, TRUE); + $this->assertEqual($testfile2->filename, $node->field_image[LANGUAGE_NONE][0]['filename'], 'File has been replaced.'); + + // Add another file to the article node. Get validation error. + $result = $this->servicesPostFile($this->endpoint->path . '/node/' . $node->nid . '/attach_file', + array($testfile1->uri), array(), array('field_name' => 'field_image')); + $this->assertEqual($result['body'], 'You cannot upload so many files.', 'Validation on cardinality works.'); + + // Update field info. Set cardinality 2. + $field_info = field_read_field('field_image'); + $field_info['cardinality'] = 2; + field_update_field($field_info); + + // Upload multiple files. + $result = $this->servicesPostFile($this->endpoint->path . '/node/' . $node->nid . '/attach_file', + array($testfile1->uri, $testfile2->uri), array(), array('field_name' => 'field_image', 'attach' => FALSE)); + $node = node_load($node->nid, TRUE); + $this->assertTrue(($testfile1->filename == $node->field_image[LANGUAGE_NONE][0]['filename']) && + ($testfile2->filename == $node->field_image[LANGUAGE_NONE][1]['filename']), 'Multiple files uploaded.'); + + // Verify total file count == 2 and also proper delta sequence in db. + $query = db_select('field_data_field_image', 'fd'); + $deltas = $query->condition('entity_type', 'node') + ->condition('bundle', $node->type) + ->condition('entity_id', $node->nid) + ->fields('fd', array('delta')) + ->execute() + ->fetchCol(0); + $this->assertTrue($deltas == array(0,1), 'Attached file deltas are sequential.'); + } + + /** + * Helper function to perform node updates. + * + * @parm $exclude_type + * Integer how should the type value be treated. + */ + function update_node($exclude_type) { + $node = $this->drupalCreateNode(); + $node_update = (array) $node; + $node_update['title'] = $this->randomName(); + $node_update['body'][LANGUAGE_NONE][0]['value'] = $this->randomName(); + + if ($exclude_type == SERVICES_NODE_TYPE_EMPTY) { + $node_update['type'] = ''; + } + elseif($exclude_type == SERVICES_NODE_TYPE_REMOVED) { + unset($node_update['type']); + } + + $responseArray = $this->servicesPut($this->endpoint->path . '/node/' . $node->nid, $node_update); + // Load node not from cache. + $nodeAfterUpdate = node_load($responseArray['body']['nid'], NULL, TRUE); + $this->assertTrue(isset($nodeAfterUpdate->nid), 'Node was successfully updated', 'NodeResource: Updated'); + $this->assertEqual($nodeAfterUpdate->title, $node_update['title'], 'Title is the same', 'NodeResource: Update'); + $this->assertEqual($nodeAfterUpdate->body[LANGUAGE_NONE][0]['value'], $node_update['body'][LANGUAGE_NONE][0]['value'], + 'Body is the same', 'NodeResource: Update'); + } + + /** + * Testing node_resource Update. + */ + public function testEndpointResourceNodeUpdate() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'bypass node access', + )); + $this->drupalLogin($this->privilegedUser); + + $this->update_node(SERVICES_NODE_TYPE_INCLUDE); + $this->update_node(SERVICES_NODE_TYPE_EMPTY); + $this->update_node(SERVICES_NODE_TYPE_REMOVED); + } + + /** + * Testing node_resource Update (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + public function testEndpointResourceNodeUpdateLegacy() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'bypass node access', + )); + $this->drupalLogin($this->privilegedUser); + $node = $this->drupalCreateNode(); + + $node_update = (array) $node; + $node_update['title'] = $this->randomName(); + $node_update['body'][LANGUAGE_NONE][0]['value'] = $this->randomName(); + + $responseArray = $this->servicesPut($this->endpoint->path . '/node/' . $node->nid, array('node' => $node_update)); + // Load node not from cache. + $nodeAfterUpdate = node_load($responseArray['body']['nid'], NULL, TRUE); + $this->assertTrue(isset($nodeAfterUpdate->nid), 'Node was successfully updated', 'NodeResource: Updated (legacy)'); + $this->assertEqual($nodeAfterUpdate->title, $node_update['title'], 'Title is the same', 'NodeResource: Update (legacy)'); + $this->assertEqual($nodeAfterUpdate->body[LANGUAGE_NONE][0]['value'], $node_update['body'][LANGUAGE_NONE][0]['value'], + 'Body is the same', 'NodeResource: Update (legacy)'); + } + + /** + * testing node_resource Update fail with no permissions + */ + public function testEndpointResourceNodeUpdatePermFail() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'create page content', + 'edit own page content', + )); + $this->drupalLogin($this->privilegedUser); + + // Create node from user no 1. + $node = $this->drupalCreateNode(array('uid' => 1)); + + // Try to update this node with different user not + // having permission to edit any story content. + $node_update = (array) $node; + $node_update['title'] = $this->randomName(); + $node_update['body'][LANGUAGE_NONE][0]['value'] = $this->randomName(); + + $responseArray = $this->servicesPut($this->endpoint->path . '/node/' . $node->nid, array('node' => $node_update)); + + $this->assertTrue(strpos($responseArray['status'], 'Access denied for user'), + 'Updating the node failed without needed permissions. This is good!', 'NodeResource: Update'); + } + + /** + * testing node_resource Update verify missing title + */ + public function testEndpointResourceNodeUpdateMissingTitle() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'bypass node access', + )); + $this->drupalLogin($this->privilegedUser); + $node = $this->drupalCreateNode(); + $node_update = array( + 'title' => '', + 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomString()))), + 'name' => $this->privilegedUser->name, + 'type' => 'page', + ); + + $responseArray = $this->servicesPut($this->endpoint->path . '/node/' . $node->nid, array('node' => $node_update)); + $this->assertTrue(strpos($responseArray['status'], 'Title field is required.'), + 'Node was not updated without title.', 'NodeResource: Update'); + } + + /** + * testing node_resource Delete + */ + public function testEndpointResourceNodeDelete() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + 'bypass node access', + )); + $this->drupalLogin($this->privilegedUser); + $node = $this->drupalCreateNode(); + $data = ''; + + $responseArray = $this->servicesDelete($this->endpoint->path . '/node/' . $node->nid, $data); + $deleted_node = node_load($node->nid, NULL, TRUE); + $this->assertTrue(empty($deleted_node), 'Node was deleted.', 'NodeResource: Deleted'); + + $responseArray = $this->servicesDelete($this->endpoint->path . '/node/' . $node->nid, $data); + + $this->assertFalse($responseArray['code'] == 200, + 'Node was deleted. It shoudlnt have been because it doesnt exist', 'NodeResource: Deleted'); + } +} + +/** + * Test create node with taxonomy terms attached. + */ +class ServicesResourceNodeTaxonomytests extends ServicesWebTestCase { + // Class variables + protected $admin_user = NULL ; + // Endpoint details. + protected $endpoint = NULL; + // Field instance. + protected $instance = NULL; + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Resource Node - taxonomy', + 'description' => 'Test the resource Node taxonomy methods and actions.', + 'group' => 'Services', + ); + } + + /** + * Implementation of setUp(). + */ + public function setUp() { + parent::setUp( + 'ctools', + 'services', + 'rest_server', + 'taxonomy' + ); + + $this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access', 'administer services')); + $this->drupalLogin($this->admin_user); + $this->vocabulary = $this->createVocabulary(); + + $field = array( + 'field_name' => 'taxonomy_' . $this->vocabulary->machine_name, + 'type' => 'taxonomy_term_reference', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($field); + + $this->instance = array( + 'field_name' => 'taxonomy_' . $this->vocabulary->machine_name, + 'bundle' => 'article', + 'entity_type' => 'node', + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance); + + $this->endpoint = $this->saveNewEndpoint(); + } + + /** + * Test that hook_node_$op implementations work correctly. + * + * Save & edit a node and assert that taxonomy terms are saved/loaded properly. + */ + function testServicesTaxonomyNode() { + // Create two taxonomy terms. + $term1 = $this->createTerm($this->vocabulary); + $term2 = $this->createTerm($this->vocabulary); + $field_name = $this->instance['field_name']; + + // Post an article. + $edit = array(); + $langcode = LANGUAGE_NONE; + $edit['title'] = $this->randomName(); + $edit["body[$langcode][0][value]"] = $this->randomName(); + $edit[$field_name][$langcode][0] = $term1->tid; + $edit['type'] = 'page'; + $edit['name'] = $this->admin_user->name; + $edit['language'] = LANGUAGE_NONE; + $responseArray = $this->servicesPost($this->endpoint->path . '/node', array('node' => $edit)); + + $nodeResourceCreateReturn = $responseArray['body']; + $this->assertTrue(isset($nodeResourceCreateReturn['nid']), 'Node was successfully created', 'NodeResource: Create'); + $newNode = node_load($nodeResourceCreateReturn['nid']); + $this->assertTrue($newNode->{$field_name}[$langcode][0]['tid'] = $term1->tid, 'Term was the same', 'Taxonomy: Create'); + + // Edit the node with a different term. + $edit[$field_name][$langcode][0] = $term2->tid; + $responseArray = $this->servicesPost($this->endpoint->path . '/node', array('node' => $edit)); + $nodeResourceCreateReturn = $responseArray['body']; + $this->assertTrue(isset($nodeResourceCreateReturn['nid']), 'Node was successfully created', 'NodeResource: Create'); + $newNode = node_load($nodeResourceCreateReturn['nid']); + $this->assertTrue($newNode->{$field_name}[$langcode][0]['tid'] = $term2->tid, 'Term was the same', 'Taxonomy: updated'); + } + + /** + * Returns a new vocabulary with random properties. + */ + function createVocabulary() { + // Create a vocabulary. + $vocabulary = new stdClass(); + $vocabulary->name = $this->randomName(); + $vocabulary->description = $this->randomName(); + $vocabulary->machine_name = drupal_strtolower($this->randomName()); + $vocabulary->help = ''; + $vocabulary->nodes = array('article' => 'article'); + $vocabulary->weight = mt_rand(0, 10); + taxonomy_vocabulary_save($vocabulary); + return $vocabulary; + } + + /** + * Returns a new term with random properties in vocabulary $vid. + */ + function createTerm($vocabulary) { + $term = new stdClass(); + $term->name = $this->randomName(); + $term->description = $this->randomName(); + // Use the first available text format. + $term->format = db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField(); + $term->vid = $vocabulary->vid; + taxonomy_term_save($term); + return $term; + } +} diff --git a/openthess/modules/services/tests/functional/ServicesResourceSystemTests.test b/openthess/modules/services/tests/functional/ServicesResourceSystemTests.test new file mode 100644 index 0000000..92388d7 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesResourceSystemTests.test @@ -0,0 +1,122 @@ +endpoint = $this->saveNewEndpoint(); + // Set up privileged user and login. + $this->privileged_user = $this->drupalCreateUser(array('get a system variable', 'set a system variable')); + $this->drupalLogin($this->privileged_user); + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Resource System', + 'description' => 'Test the resource System methods.', + 'group' => 'Services', + ); + } + + /** + * Test connect method. + */ + function testSystemConnect() { + $path = $this->endpoint->path; + // Call as authenticated user. + $response = $this->servicesPost($path . '/system/connect'); + $response_user = $response['body']->user; + $this->assertEqual($response_user->uid, $this->privileged_user->uid, 'User account received for authenticated user.', 'SystemResource: Connect'); + + $this->drupalLogout(); + // Call as anonymous user. + $response = $this->servicesPost($path . '/system/connect'); + $response_user = $response['body']->user; + $this->assertEqual($response_user->uid, 0, 'User account received for anonymous user.', 'SystemResource: Connect'); + } + + /** + * Test get_variable method. + */ + function testSystemGetVariable() { + $path = $this->endpoint->path; + + $name = $this->randomName(); + $value = $this->randomString(); + variable_set($name, $value); + + // Get already set variable. + $response = $this->servicesPost($path . '/system/get_variable', array('name' => $name, 'default' => $this->randomString())); + $this->assertEqual($value, $response['body'], 'Variable get value.', 'SystemResource: get_variable'); + + $name = $this->randomName(); + $default = $this->randomString(); + + // Get not defined variable. Ensure we get back default value. + $response = $this->servicesPost($path . '/system/get_variable', array('name' => $name, 'default' => $default)); + $this->assertEqual($default, $response['body'], 'Variable get value default.', 'SystemResource: get_variable'); + } + + /** + * Test set_variable method. + */ + function testSystemSetVariable() { + $path = $this->endpoint->path; + + $name = $this->randomName(); + $value = $this->randomString(); + + $response = $this->servicesPost($path . '/system/set_variable', array('name' => $name, 'value' => $value)); + + // We can't use variable_get as variables get cached to global variable. + $variable = unserialize(db_query('SELECT value FROM {variable} WHERE name = :name', array(':name' => $name))->fetchField()); + + $this->assertEqual($value, $variable, 'Variable set value.', 'SystemResource: set_variable'); + } + + /** + * Test set_variable method. + */ + function testSystemDelVariable() { + $path = $this->endpoint->path; + + // Set a random variable. + $name = $this->randomName(); + $value = $this->randomString(); + variable_set($name, $value); + + // Delete the variable via del_variable. + $response = $this->servicesPost($path . '/system/del_variable', array('name' => $name)); + + // We can't use variable_get as variables get cached to global variable. + $newvalue = $this->randomString(); + $response = $this->servicesPost($path . '/system/get_variable', array('name' => $name, 'default' => $newvalue)); + $this->assertEqual($newvalue, $response['body'], 'Variable deleted.', 'SystemResource: get_variable'); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesResourceTaxonomyTests.test b/openthess/modules/services/tests/functional/ServicesResourceTaxonomyTests.test new file mode 100644 index 0000000..0575db1 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesResourceTaxonomyTests.test @@ -0,0 +1,672 @@ +endpoint = $this->saveNewEndpoint(); + // Set up privileged user and login. + $this->privileged_user = $this->drupalCreateUser(array('administer taxonomy', 'access content')); + $this->drupalLogin($this->privileged_user); + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Resource Taxonomy', + 'description' => 'Test the resource Taxonomy methods and actions.', + 'group' => 'Services', + ); + } + public function testTaxonomyVocabularyIndex() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + )); + $this->drupalLogin($this->privilegedUser); + + + // Create a set of taxonomy vocabularys. The taxonomy resource returns 20 vocabularys at a time, + // so we create two pages and a half worth. + $vocabularys = array(); + $count = 50; + for ($i = 0; $i < $count; $i++) { + $vocabulary = $this->createVocabulary(); + $vocabularys[$vocabulary['vid']] = $vocabulary; + } + $vocabulary = taxonomy_vocabulary_load(1); + $vocabularys[1] = (array) $vocabulary; + // Get the content. + $page_count = ceil(count($vocabularys) / 20); + $retrieved_terms = array(); + for ($page = 0; $page < $page_count; $page++) { + $responseArray = $this->servicesGet($this->endpoint->path . '/taxonomy_vocabulary', array('page' => $page, 'fields' => 'vid,name')); + $this->assertTrue(count($responseArray['body']) <= 20, 'Correct number of items returned'); + + // Store the returned comment IDs. + foreach ($responseArray['body'] as $vocabulary) { + if (isset($retrieved_vocabularys[$vocabulary->vid])) { + $this->fail(format_string('Duplicate vocabulary @vid returned.', array('@vid' => $vocabulary->vid))); + } + $retrieved_vocabularys[$vocabulary->vid] = TRUE; + + $this->assertTrue($vocabularys[$vocabulary->vid]['name'] == $vocabulary->name, + 'Successfully received vocabulary Name info', 'TaxonomyVocabularyResource: Index'); + } + } + // We should have got all the comments. + $expected_vids = array_keys($vocabularys); + sort($expected_vids); + $retrieved_vids = array_keys($retrieved_vocabularys); + sort($retrieved_vids); + $this->assertEqual($expected_vids, $retrieved_vids, 'Retrieved all vocabularys'); + + // The n+1 page should be empty. + $responseArray = $this->servicesGet($this->endpoint->path . '/taxonomy_vocabulary', array('page' => $page_count + 1)); + $this->assertEqual(count($responseArray['body']), 0, 'The n+1 page is empty'); + } + public function testTaxonomyTermIndex() { + // Create and log in our privileged user. + $this->privilegedUser = $this->drupalCreateUser(array( + 'administer services', + )); + $this->drupalLogin($this->privilegedUser); + + + // Create a set of taxonomy terms. The taxonomy resource returns 20 terms at a time, + // so we create two pages and a half worth. + $terms = array(); + $count = 50; + $vocabulary = $this->createVocabulary(); + for ($i = 0; $i < $count; $i++) { + $term = $this->createTerm($vocabulary['vid']); + $terms[$term['tid']] = $term; + } + + // Get the content. + $page_count = ceil(count($terms) / 20); + $retrieved_terms = array(); + for ($page = 0; $page < $page_count; $page++) { + $responseArray = $this->servicesGet($this->endpoint->path . '/taxonomy_term', array('page' => $page, 'fields' => 'tid,name')); + $this->assertTrue(count($responseArray['body']) <= 20, 'Correct number of items returned'); + + // Store the returned comment IDs. + foreach ($responseArray['body'] as $term) { + if (isset($retrieved_terms[$term->tid])) { + $this->fail(format_string('Duplicate term @tid returned.', array('@tid' => $term->tid))); + } + $retrieved_terms[$term->tid] = TRUE; + + $this->assertTrue($terms[$term->tid]['name'] == $term->name, 'Successfully received Term Name info', 'TaxonomyTermResource: Index'); + } + } + // We should have got all the comments. + $expected_tids = array_keys($terms); + sort($expected_tids); + $retrieved_tids = array_keys($retrieved_terms); + sort($retrieved_tids); + $this->assertEqual($expected_tids, $retrieved_tids, 'Retrieved all terms'); + + // The n+1 page should be empty. + $responseArray = $this->servicesGet($this->endpoint->path . '/taxonomy_term', array('page' => $page_count + 1)); + $this->assertEqual(count($responseArray['body']), 0, 'The n+1 page is empty'); + taxonomy_vocabulary_delete($vocabulary['vid']); + } + + /** + * Test taxonomy vocabulary create method. + */ + function testVocabularyCreate() { + $path = $this->endpoint->path; + + $vocabulary = array( + 'name' => $this->randomName(), + 'machine_name'=> $this->randomName(), + 'description' => $this->randomString(), + 'hierarchy' => 1, + 'module' => 'services', + 'weight' => 0, + ); + + $response = $this->servicesPost($path . '/taxonomy_vocabulary', $vocabulary); + + $query = db_select('taxonomy_vocabulary', 'v') + ->fields('v', array('vid')) + ->condition('v.name', $vocabulary['name']); + $vid = $query->execute()->fetchField(); + + $vocabulary_load = (array)taxonomy_vocabulary_load($vid); + $vocabulary_intersect = array_intersect_assoc($vocabulary, $vocabulary_load); + + $this->assertEqual($vocabulary, $vocabulary_intersect, 'Taxonomy vocabulary created properly.', 'TaxonomyVocabularyResource: Create'); + } + + /** + * Test taxonomy vocabulary create method (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testVocabularyCreateLegacy() { + $path = $this->endpoint->path; + + $vocabulary = array( + 'name' => $this->randomName(), + 'machine_name'=> $this->randomName(), + 'description' => $this->randomString(), + 'hierarchy' => 1, + 'module' => 'services', + 'weight' => 0, + ); + + $response = $this->servicesPost($path . '/taxonomy_vocabulary', array('vocabulary' => $vocabulary)); + + $query = db_select('taxonomy_vocabulary', 'v') + ->fields('v', array('vid')) + ->condition('v.name', $vocabulary['name']); + $vid = $query->execute()->fetchField(); + + $vocabulary_load = (array)taxonomy_vocabulary_load($vid); + $vocabulary_intersect = array_intersect_assoc($vocabulary, $vocabulary_load); + + $this->assertEqual($vocabulary, $vocabulary_intersect, 'Taxonomy vocabulary created properly.', 'TaxonomyVocabularyResource: Create (Legacy)'); + } + + /** + * Test taxonomy vocabulry retrieve method. + */ + function testVocabularyRetrieve() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + $vid = $vocabulary['vid']; + + $response = $this->servicesGet($path . '/taxonomy_vocabulary/' . $vid); + $vocabulary_retrieve = (array)$response['body']; + + $vocabulary_intersect = array_intersect_assoc($vocabulary, $vocabulary_retrieve); + + $this->assertEqual($vocabulary, $vocabulary_intersect, 'Taxonomy vocabulary retrieved properly.', 'TaxonomyVocabularyResource: Retrieve'); + } + + /** + * Test taxonomy vocabulary update. + */ + function testVocabularyUpdate() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + $vid = $vocabulary['vid']; + + $vocabulary['name'] = $this->randomName(); + $vocabulary['description'] = $this->randomString(); + + $response = $this->servicesPUT($path . '/taxonomy_vocabulary/' . $vid, $vocabulary); + + // Load vocabulary from database. We use entity_load to reset static cache. + $vocabularies_load = entity_load('taxonomy_vocabulary', array($vid), array(), TRUE); + $vocabulary_load = (array)array_pop($vocabularies_load); + + $vocabulary_intersect = array_intersect_assoc($vocabulary, $vocabulary_load); + $this->assertEqual($vocabulary, $vocabulary_intersect, 'Taxonomy vocabulary updated properly.', 'TaxonomyVocabularyResource: Update'); + } + + /** + * Test taxonomy vocabulary update (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testVocabularyUpdateLegacy() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + $vid = $vocabulary['vid']; + + $vocabulary['name'] = $this->randomName(); + $vocabulary['description'] = $this->randomString(); + + $response = $this->servicesPUT($path . '/taxonomy_vocabulary/' . $vid, array('vocabulary' => $vocabulary)); + + // Load vocabulary from database. We use entity_load to reset static cache. + $vocabularies_load = entity_load('taxonomy_vocabulary', array($vid), array(), TRUE); + $vocabulary_load = (array)array_pop($vocabularies_load); + + $vocabulary_intersect = array_intersect_assoc($vocabulary, $vocabulary_load); + $this->assertEqual($vocabulary, $vocabulary_intersect, + 'Taxonomy vocabulary updated properly.', 'TaxonomyVocabularyResource: Update (Legacy)'); + } + + /** + * Test taxonomy vocabulary delete method. + */ + function testVocabularyDelete() { + $path = $this->endpoint->path; + $vocabulary = $this->createVocabulary(); + $vid = $vocabulary['vid']; + + $this->servicesDelete($path . '/taxonomy_vocabulary/' . $vid); + + // Load vocabulary from database. We use entity_load to reset static cache. + $vocabularies_load = entity_load('taxonomy_vocabulary', array($vid), array(), TRUE); + $vocabulary_load = (array)array_pop($vocabularies_load); + + $this->assertTrue(empty($vocabulary_load), 'Taxonomy vocabulary deleted properly.', 'TaxonomyVocabularyResource: Delete'); + } + + /** + * Test taxonomy vocabulary getTree method. + */ + function testVocabularyGetTree() { + $path = $this->endpoint->path; + $vocabulary = $this->createVocabulary(); + $vid = $vocabulary['vid']; + + $part_tree_parent = 0; + + // Generate taxonomy tree. + for ($i = 0; $i < 10; $i++) { + $query = db_select('taxonomy_term_data', 'td') + ->fields('td', array('tid')) + ->condition('td.vid', $vid) + ->orderRandom() + ->range(0,1); + $tid = $query->execute()->fetchField(); + $parent = rand(0, 1) * $tid; + $edit = (object)array( + 'name' => $this->randomName(), + 'parent' => $parent, + 'vid' => $vid, + ); + taxonomy_term_save($edit); + + if (!empty($parent)) { + $part_tree_parent = $parent; + } + } + // Add term as grandchild to test maxdepth. + $children = taxonomy_get_children($part_tree_parent); + $edit = (object)array( + 'name' => $this->randomName(), + 'parent' => key($children), + 'vid' => $vid, + ); + taxonomy_term_save($edit); + + // Compare full tree. + $vocabulary_tree = taxonomy_get_tree($vid); + $response = $this->servicesPost($path . '/taxonomy_vocabulary/getTree', array('vid' => $vid)); + $vocabulary_tree_response = $response['body']; + $this->assertEqual($vocabulary_tree, $vocabulary_tree_response, + 'Vocabulary full tree received properly.', 'TaxonomyVocabularyResource: getTree'); + + // Compare full tree with loading of full entities. + $vocabulary_tree = taxonomy_get_tree($vid, 0, NULL, TRUE); + $response = $this->servicesPost($path . '/taxonomy_vocabulary/getTree', array('vid' => $vid, 'load_entities' => 1)); + $vocabulary_tree_response = $response['body']; + $this->assertEqual($vocabulary_tree, $vocabulary_tree_response, + 'Vocabulary full tree with loaded entities received properly.', 'TaxonomyVocabularyResource: getTree'); + + // Compare part tree. + $vocabulary_tree = taxonomy_get_tree($vid, $part_tree_parent); + $response = $this->servicesPost($path . '/taxonomy_vocabulary/getTree', + array('vid' => $vid, 'parent' => $part_tree_parent)); + $vocabulary_tree_response = $response['body']; + $this->assertEqual($vocabulary_tree, $vocabulary_tree_response, + 'Vocabulary part tree received properly.', 'TaxonomyVocabularyResource: getTree'); + + // Compare part tree with maxdepth = 1. + $vocabulary_tree = taxonomy_get_tree($vid, $part_tree_parent, 1); + $response = $this->servicesPost($path . '/taxonomy_vocabulary/getTree', + array('vid' => $vid, 'parent' => $part_tree_parent, 'maxdepth' => 1)); + $vocabulary_tree_response = $response['body']; + $this->assertEqual($vocabulary_tree, $vocabulary_tree_response, + 'Vocabulary part tree with depth received properly.', 'TaxonomyVocabularyResource: getTree'); + + // Compare part tree with maxdepth = 1 and loading of full entities. + $vocabulary_tree = taxonomy_get_tree($vid, $part_tree_parent, 1, TRUE); + $response = $this->servicesPost($path . '/taxonomy_vocabulary/getTree', + array('vid' => $vid, 'parent' => $part_tree_parent, 'maxdepth' => 1, 'load_entities' => 1)); + $vocabulary_tree_response = $response['body']; + $this->assertEqual($vocabulary_tree, $vocabulary_tree_response, + 'Vocabulary part tree with depth and loaded entities received properly.', 'TaxonomyVocabularyResource: getTree'); + } + + /** + * Test taxonomy term create method. + */ + function testTermCreate() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + + $term = array( + 'vid' => $vocabulary['vid'], + 'name' => $this->randomName(), + 'description' => $this->randomString(), + 'weight' => rand(0, 100), + 'parent' => NULL, + ); + + $response = $this->servicesPost($path . '/taxonomy_term', $term); + + // Load term by name. + $term_by_name = (array)current(taxonomy_get_term_by_name($term['name'])); + $term_intersect = array_intersect_assoc($term, $term_by_name); + + // As term_intersect will not have parent, we unset this property. + $term_data = $term; + unset($term_data['parent']); + + $this->assertEqual($term_data, $term_intersect, 'Taxonomy term created properly.', 'TaxonomyTermResource: Create'); + } + + /** + * Test taxonomy term create method (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testTermCreateLegacy() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + + $term = array( + 'vid' => $vocabulary['vid'], + 'name' => $this->randomName(), + 'description' => $this->randomString(), + 'weight' => rand(0, 100), + 'parent' => NULL, + ); + + $response = $this->servicesPost($path . '/taxonomy_term', array('term' => $term)); + + // Load term by name. + $term_by_name = (array)current(taxonomy_get_term_by_name($term['name'])); + $term_intersect = array_intersect_assoc($term, $term_by_name); + + // As term_intersect will not have parent, we unset this property. + $term_data = $term; + unset($term_data['parent']); + + $this->assertEqual($term_data, $term_intersect, + 'Taxonomy term created properly.', 'TaxonomyTermResource: Create (Legacy)'); + } + + /** + * Test taxonomy term retrieve method. + */ + function testTermRetrieve() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + $term = $this->createTerm($vocabulary['vid']); + + $response = $this->servicesGet($path . '/taxonomy_term/' . $term['tid']); + $term_retrieve = (array)$response['body']; + + $this->assertEqual($term_retrieve, $term, 'Taxonomy term retrieved properly.', 'TaxonomyTermResource: Retrieve'); + } + + /** + * Test taxonomy term update method. + */ + function testTermUpdate() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + $term = $this->createTerm($vocabulary['vid']); + + $term_update_data = array( + 'name' => $this->randomName(), + 'vid' => $term['vid'], + ); + + $this->servicesPut($path . '/taxonomy_term/' . $term['tid'], $term_update_data); + + $term_update = (array)current(entity_load('taxonomy_term', array($term['tid']), array(), TRUE)); + + // Ensure that terms have different names but same tid. + $this->assertTrue(($term['tid'] == $term_update['tid']) && ($term['name'] != $term_update['name']), + 'Taxonomy term updated properly.', 'TaxonomyTermResource: Update'); + } + + /** + * Test taxonomy term update method (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testTermUpdateLegacy() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + $term = $this->createTerm($vocabulary['vid']); + + $term_update_data = array( + 'name' => $this->randomName(), + 'vid' => $term['vid'], + ); + + $this->servicesPut($path . '/taxonomy_term/' . $term['tid'], array('term' => $term_update_data)); + + $term_update = (array)current(entity_load('taxonomy_term', array($term['tid']), array(), TRUE)); + + // Ensure that terms have different names but same tid. + $this->assertTrue(($term['tid'] == $term_update['tid']) && ($term['name'] != $term_update['name']), + 'Taxonomy term updated properly.', 'TaxonomyTermResource: Update (Legacy)'); + } + + /** + * Test taxonomy term delete method. + */ + function testTermDelete() { + $path = $this->endpoint->path; + + $vocabulary = $this->createVocabulary(); + $term = $this->createTerm($vocabulary['vid']); + + $this->servicesDelete($path . '/taxonomy_term/' . $term['tid']); + + $term_load = entity_load('taxonomy_term', array($term['tid']), array(), TRUE); + $this->assertTrue(empty($term_load), 'Taxonomy term deleted properly.', 'TaxonomyTermResource: Delete'); + } + + /** + * Test taxonomy term selectNodes method. + */ + function testTermSelectNodes() { + $path = $this->endpoint->path; + + $vocabulary = (object)array( + 'name' => $this->randomName(), + 'machine_name'=> 'text_vocabulary', + 'description' => $this->randomString(), + 'help' => $this->randomString(), + 'relations' => 1, + 'hierarchy' => 1, + 'multiple' => 1, + 'required' => 0, + 'module' => 'services', + 'weight' => 0, + 'nodes' => array('page' => 'page'), + ); + taxonomy_vocabulary_save($vocabulary); + + $query = db_select('taxonomy_vocabulary', 'v') + ->fields('v', array('vid')) + ->condition('v.name', $vocabulary->name); + $vid = $query->execute()->fetchField(); + + $term1 = $this->createTerm($vid); + $term2 = $this->createTerm($vid); + + $nodes = array(); + $nodes_term1 = array(); + $nodes_term2 = array(); + $nodes_term1_term2 = array(); + $nodes_noterm = array(); + + $field_name = 'taxonomy_' . $vocabulary->machine_name; + + // Create field for term. + $field = array( + 'field_name' => $field_name, + 'type' => 'taxonomy_term_reference', + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($field); + + $instance = array( + 'field_name' => $field_name, + 'entity_type' => 'node', + 'label' => $vocabulary->name, + 'bundle' => 'page', + 'required' => TRUE, + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + ), + ); + field_create_instance($instance); + + node_types_rebuild(); + + // Create 7 page nodes with term1 attached. + for ($i = 0; $i < 7; $i++) { + $node = $this->drupalCreateNode(array($field_name => array(LANGUAGE_NONE => array(array('tid' => $term1['tid']))))); + $nodes[$node->nid] = $node; + $nodes_term1[] = $node->nid; + } + + // Create 7 page nodes with term2 attached. + for ($i = 0; $i < 7; $i++) { + $node = $this->drupalCreateNode(array($field_name => array(LANGUAGE_NONE => array(array('tid' => $term2['tid']))))); + $nodes[$node->nid] = $node; + $nodes_term2[] = $node->nid; + } + + // Create 7 page nodes with both term1 and term2 attached. + for ($i = 0; $i < 7; $i++) { + $node = $this->drupalCreateNode(array($field_name => array(LANGUAGE_NONE => array(array('tid' => $term1['tid']), array('tid' => $term2['tid']))))); + $nodes[$node->nid] = $node; + $nodes_term1_term2[] = $node->nid; + } + + // Create 7 page nodes without any terms. + for ($i = 0; $i < 7; $i++) { + $node = $this->drupalCreateNode(); + $nodes[$node->nid] = $node; + $nodes_noterm[] = $node->nid; + } + + // If pager is FALSE query is limited by 'feed_default_items' variable. + variable_set('feed_default_items', 100); + + // Select 14 nodes with term1 attached. + $response = $this->servicesPost($path . '/taxonomy_term/selectNodes', array('tid' => $term1['tid'], 'pager' => FALSE)); + $response_nodes = $this->getNodeNids($response['body']); + sort($response_nodes); + + $term1_nodes = array_merge($nodes_term1, $nodes_term1_term2); + sort($term1_nodes); + + $this->assertEqual($response_nodes, $term1_nodes, 'selectNodes selected proper nodes by one term.', 'TaxonomyTermResource: selectNodes'); + + // Ensure pager works. + $response = $this->servicesPost($path . '/taxonomy_term/selectNodes', array('tid' => $term1['tid'], 'pager' => TRUE)); + $this->assertEqual(count($response['body']), 10, 'selectNodes pager works.', 'TaxonomyTermResource: selectNodes'); + + // AND or OR tests are not applicable as taxonomy_select_nodes() does not accept operators. + } + + /** + * Helper. Create taxonomy vocabulary. + */ + function createVocabulary() { + $vocabulary = (object)array( + 'name' => $this->randomName(), + 'machine_name'=> $this->randomName(), + 'description' => $this->randomString(), + 'hierarchy' => 1, + 'module' => 'services', + 'weight' => 0, + ); + taxonomy_vocabulary_save($vocabulary); + $query = db_select('taxonomy_vocabulary', 'v') + ->fields('v', array('vid')) + ->condition('v.name', $vocabulary->name); + $vid = $query->execute()->fetchField(); + + return (array)taxonomy_vocabulary_load($vid); + } + + /** + * Helper. Create taxonomy term. + */ + function createTerm($vid) { + $term = (object)array( + 'vid' => $vid, + 'name' => $this->randomName(), + 'description' => $this->randomString(), + 'weight' => rand(0, 100), + 'parent' => NULL, + ); + taxonomy_term_save($term); + + // Load term by name. + return (array)current(taxonomy_get_term_by_name($term->name)); + } + + /** + * Helper. Get array of nids from nodes array. + */ + function getNodeNids($nodes) { + $nodes = (array)$nodes; + $return = array(); + foreach ($nodes as $node) { + if (isset($node->nid)) { + $return[] = $node->nid; + } + } + return $return; + } +} diff --git a/openthess/modules/services/tests/functional/ServicesResourceUserTests.test b/openthess/modules/services/tests/functional/ServicesResourceUserTests.test new file mode 100644 index 0000000..6e94d89 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesResourceUserTests.test @@ -0,0 +1,620 @@ +endpoint = $this->saveNewEndpoint(); + // Set up privileged user and login. + $this->privileged_user = $this->drupalCreateUser(array('administer users', 'access user profiles')); + $this->regular_user = $this->drupalCreateUser(array('access user profiles')); + $this->drupalLogin($this->privileged_user); + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Resource User', + 'description' => 'Test the resource User methods and actions.', + 'group' => 'Services', + ); + } + + /** + * Test create method. + * + * Create user, load user, try ti create user without email. + */ + function testCreateUser() { + // Create user. + $user = array(); + $user['name'] = $this->randomName(); + $user['mail'] = $user['name'] . '@example.com'; + $user['pass'] = user_password(); + $user['status'] = 1; + + $response = $this->servicesPost($this->endpoint->path . '/user', $user); + $account = $response['body']; + $this->assertTrue(!empty($account['uid']), 'User has been create successfully.', 'UserResource: Create'); + + // Load user. + $user_load = user_load($account['uid']); + $this->assertTrue(!empty($user_load), 'Newly created user has been loaded successfully.', 'UserResource: Create'); + + // Try to create user without email. + $user = array(); + $user['name'] = $this->randomName(); + $user['pass'] = user_password(); + $user['status'] = 1; + $response = $this->servicesPost($this->endpoint->path . '/user', $user); + $this->assertTrue(strpos($response['status'], 'E-mail address field is required') !== FALSE, + 'It is not possible to create user without email.', 'UserResource: Create'); + } + + /** + * Test create method (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testCreateUserLegacy() { + // Create user. + $user = array(); + $user['name'] = $this->randomName(); + $user['mail'] = $user['name'] . '@example.com'; + $user['pass'] = user_password(); + $user['status'] = 1; + + $response = $this->servicesPost($this->endpoint->path . '/user', array('account' => $user)); + $account = $response['body']; + $this->assertTrue(!empty($account['uid']), 'User has been create successfully.', 'UserResource: Create (Legacy)'); + + // Load user. + $user_load = user_load($account['uid']); + $this->assertTrue(!empty($user_load), 'Newly created user has been loaded successfully.', 'UserResource: Create (Legacy)'); + + // Try to create user without email. + $user = array(); + $user['name'] = $this->randomName(); + $user['pass'] = user_password(); + $user['status'] = 1; + $response = $this->servicesPost($this->endpoint->path . '/user', array('account' => $user)); + $this->assertTrue(strpos($response['status'], 'E-mail address field is required') !== FALSE, + 'It is not possible to create user without email.', 'UserResource: Create (Legacy)'); + } + + /** + * Test register method. + * + * Register user, load user. + */ + function testRegisterUser() { + // Verify logged out state can create users + $this->drupalLogout(); + + $user = array(); + $user['name'] = $this->randomName(); + $user['mail'] = $user['name'] . '@example.com'; + + $response = $this->servicesPost($this->endpoint->path . '/user/register', $user); + $account = $response['body']; + + $this->assertTrue(!empty($account['uid']), 'User has been create successfully.', 'UserResource: Create'); + + // Load user. + $user_load = user_load($account['uid']); + $this->assertTrue(!empty($user_load), 'Newly created user has been loaded successfully.', 'UserResource: Create'); + } + + /** + * Test register method (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testRegisterUserLegacy() { + //Verify logged out state can create users + $this->drupalLogout(); + + $user = array(); + $user['name'] = $this->randomName(); + $user['mail'] = $user['name'] . '@example.com'; + + $response = $this->servicesPost($this->endpoint->path . '/user/register', array('account' => $user)); + $account = $response['body']; + + $this->assertTrue(!empty($account['uid']), 'User has been create successfully.', 'UserResource: Create (Legacy)'); + + // Load user. + $user_load = user_load($account['uid']); + $this->assertTrue(!empty($user_load), 'Newly created user has been loaded successfully.', 'UserResource: Create (Legacy)'); + } + + /** + * Test retrieve method. + */ + function testRetrieveUser() { + $response = $this->servicesGET($this->endpoint->path . '/user/' . $this->privileged_user->uid); + $account = $response['body']; + + $users_are_the_same = ($account->name == $this->privileged_user->name) + && ($account->mail = $this->privileged_user->mail) + && ($account->roles = $this->privileged_user->roles); + $this->assertTrue($users_are_the_same, 'Retrieved user is the same as created.', 'UserResource: Retrieve'); + } + /** + * Test updateing a username with administer users permission #1853592. + * + * Create user, update email. + */ + function testUpdateUserName() { + // Create user. + $account = $this->drupalCreateUser(); + $name = $this->randomName(); + // Update mail of the user. + $updated_account = array( + 'name' => $name, + ); + $response = $this->servicesPut($this->endpoint->path . '/user/' . $account->uid, $updated_account); + + $user_load = user_load($account->uid); + $this->assertEqual($name, $user_load->name, + 'You are allowed to change a username as administer users perm.', + 'User Resource : Test to check for drupal.org issue #1853592'); + } + + /** + * Test update method. + * + * Check to see if a regular user can change another user's role. + */ + function testUpdateUserRolesWithRegularAccount() { + // Create user. + $account = $this->drupalCreateUser(); + $this->drupalLogout(); + $this->drupalLogin($this->regular_user); + // Update the roles of the user. + $updated_account = array( + 'mail' => $this->randomName() . '@example.com', + 'pass' => $this->randomString(), + 'roles' => array( 3 => 'adminstrator'), + ); + $response = $this->servicesPut($this->endpoint->path . '/user/' . $account->uid, $updated_account); + + $user_load = user_load($account->uid); + //verify they are not allowed. + $this->assertEqual($response['body'], 'Access denied for user ' . $this->regular_user->name, + 'Regular user CANNOT update roles', 'UserResource: Update'); + } + + /** + * Test update own roles method. + * + * Check to see if a regular user can change their own role. + */ + function testUpdateUserOwnUserRoles() { + // Create user with minimal permission + $account = $this->drupalCreateUser(); + $this->drupalLogout(); + // Login + $this->drupalLogin($account); + + // Not strictly necessary but illustrates the problem + $role_name = $this->randomName(); + $role_rid = $this->drupalCreateRole(array('administer users'), $role_name); + + $user_load_before = user_load($account->uid); + + // Update the roles of the user. + $updated_account = array( + 'roles' => array($role_rid => $role_name), + ); + + $response = $this->servicesPut($this->endpoint->path . '/user/' . $account->uid, $updated_account); + + $user_load_after = user_load($account->uid, TRUE); + + $this->assertEqual($response['code'], 200, 'Update will should appear to succeed as the roles will be ignored', 'UserResource'); + + // The roles must remain unchanged + $this->assertEqual($response['body']['roles'], $user_load_before->roles, 'Response shows roles unchanged', 'UserResource'); + $this->assertEqual($user_load_before->roles, $user_load_after->roles, 'User roles have not been changed', 'UserResource'); + } + + /** + * Test update method. + * + * Create user, update email. + */ + function testUpdateUser() { + // Create user. + $account = $this->drupalCreateUser(); + + // Update mail of the user. + $updated_account = array( + 'mail' => $this->randomName() . '@example.com', + 'pass' => $this->randomString(), + ); + $response = $this->servicesPut($this->endpoint->path . '/user/' . $account->uid, $updated_account); + + $user_load = user_load($account->uid); + $this->assertEqual($updated_account['mail'], $user_load->mail, + 'User details have been updated successfully', 'UserResource: Update'); + $this->assertTrue(user_check_password($updated_account['pass'], $user_load), + 'Password check succeeds.', 'UserResource: Update'); + } + + /** + * Test update method (Legacy). + * + * TODO: To be removed in future version. + * @see http://drupal.org/node/1083242 + */ + function testUpdateUserLegacy() { + // Create user. + $account = $this->drupalCreateUser(); + + // Update mail of the user. + $updated_account = array( + 'mail' => $this->randomName() . '@example.com', + 'pass' => $this->randomString(), + ); + $response = $this->servicesPut($this->endpoint->path . '/user/' . $account->uid, array('data' => $updated_account)); + + $user_load = user_load($account->uid); + $this->assertEqual($updated_account['mail'], $user_load->mail, + 'User details have been updated successfully', 'UserResource: Update (Legacy)'); + $this->assertTrue(user_check_password($updated_account['pass'], $user_load), + 'Password check succeeds.', 'UserResource: Update (Legacy)'); + } + + /** + * Test delete method. + */ + function testDeleteUser() { + // Create user. + $account = $this->drupalCreateUser(); + + // Delete user. + $response = $this->servicesDelete($this->endpoint->path . '/user/' . $account->uid); + + $user_load = user_load($account->uid); + $this->assertTrue(empty($user_load), 'User has been deleted successfully.', 'UserResource: Delete'); + } + + /** + * Test cancel method. + */ + function testCancelUser() { + // Create our privileged user. + $account = $this->drupalCreateUser(array('administer services')); + + // Cancel user. + $response = $this->servicesPost($this->endpoint->path . '/user/' . $account->uid . '/cancel'); + $this->assertTrue($response['body'], + 'Resource has to cancel user has been called successfully.', + 'UserResource: Cancel'); + + $user_load = user_load($account->uid); + $this->assertFalse($user_load->status, 'User has been canceled successfully.', 'UserResource: Cancel'); + } + + /** + * Test cant cancel user 1. + */ + function testCancelAdmin() { + // Cancel user. + $response = $this->servicesPost($this->endpoint->path . '/user/1/cancel'); + $this->assertEqual($response['code'], 403, + 'Services successfully blocked cancel of user 1', 'UserResource: Cancel'); + + $user_load = user_load(1); + $this->assertTrue(!empty($user_load), 'User 1 still exits and has not deleted, as this is not allowed.', 'UserResource: Cancel'); + } + + /** + * Test password_reset method. + */ + function testPasswordReset() { + // Create user. + $account = $this->drupalCreateUser(array('administer services')); + + // Password Reset user. + $response = $this->servicesPost($this->endpoint->path . '/user/' . $account->uid . '/password_reset'); + $this->assertTrue($response['body'], + 'Resource has to reset a users password has been called successfully.', + 'UserResource: password_reset'); + + $user_load = user_load($account->uid); + $this->assertFalse(user_check_password($account->pass, $user_load), + 'Password successfully changed.', 'UserResource: password_reset'); + } + + /** + * Test password_reset method. + */ + function testResendWelcomeEmail() { + // Create user. + $account = $this->drupalCreateUser(array('administer services')); + + // Password Reset user. + $response = $this->servicesPost($this->endpoint->path . '/user/' . $account->uid . '/resend_welcome_email'); + $this->assertTrue($response['body'], + 'Resource has to resent a users welcome email has been called successfully.', + 'UserResource: resend_welcome_email'); + // Not sure how to test mail actually sent. + } + + /** + * Test delete system user method. + */ + function testDeleteSystemUser() { + // Delete user 0. + $response = $this->servicesDelete($this->endpoint->path . '/user/0'); + + $this->assertTrue(strpos($response['code'], '404') !== FALSE, + 'Anonymous user was not deleted.', 'UserResource: Delete'); + + // Delete user 1. + $response = $this->servicesDelete($this->endpoint->path . '/user/1'); + + $this->assertTrue(strpos($response['status'], 'The admin user cannot be deleted.') !== FALSE, + 'Admin user was not deleted.', 'UserResource: Delete'); + } + + /** + * Test index method. + * + * Create several users list them. List one user by name. + */ + function testUserIndex() { + // Create several users. + $accounts = array(); + for ($i = 0; $i < 5; $i++) { + $account = $this->drupalCreateUser(); + $accounts[$account->uid] = $account; + } + + $accounts_copy = $accounts; + + $response = $this->servicesGet($this->endpoint->path . '/user', array('fields' => 'uid,name,mail')); + $response_accounts = $response['body']; + + foreach ($response_accounts as $response_account) { + // We do not check anonymous and admin users. + if ($response_account->uid < 2) { + continue; + } + // If name and email are the same we believe that accounts are the same. + if (isset($accounts[$response_account->uid])) { + $saved_account = $accounts[$response_account->uid]; + if ($response_account->name == $saved_account->name && $response_account->mail == $saved_account->mail) { + unset($accounts_copy[$response_account->uid]); + } + } + } + + $this->assertTrue(empty($accounts_copy), 'Users were listed properly.', 'UserResource: Index'); + + // Retrieve all the users using a list of uids. + $response = $this->servicesGet($this->endpoint->path . '/user', + array('parameters' => array('uid' => implode(',', array_keys($accounts))))); + + $response_accounts = $response['body']; + + $accounts_copy = $accounts; + + foreach ($response_accounts as $response_account) { + // If name and email are the same we believe that accounts are the same. + if (isset($accounts[$response_account->uid])) { + $saved_account = $accounts[$response_account->uid]; + if ($response_account->name == $saved_account->name && $response_account->mail == $saved_account->mail) { + unset($accounts_copy[$response_account->uid]); + } + } + } + + $this->assertTrue(empty($accounts_copy), 'Users were listed properly.', 'UserResource: Index'); + + $accounts_copy = $accounts; + $account = array_pop($accounts_copy); + + // Get user with specific name. + $response = $this->servicesGet($this->endpoint->path . '/user', array('parameters' => array('name' => $account->name))); + $response_accounts = $response['body']; + $response_account = current($response['body']); + + $proper_answer = count($response_accounts) == 1 + && $response_account->name == $account->name; + $this->assertTrue($proper_answer, 'User was listed by name properly.', 'UserResource: Index'); + } + + /** + * Test login method. + * + * Create user. Login. Try to login with another user (to get error). + * Login with wrong credentials (to get error). + */ + function testUserLogin() { + $account = $this->drupalCreateUser(); + + // Logout first. + $this->drupalLogout(); + + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $account->pass_raw)); + + $response_data = $response['body']; + + $proper_answer = isset($response_data->sessid) + && isset($response_data->user) + && $response_data->user->name == $account->name; + $this->assertTrue($proper_answer, 'User successfully logged in.', 'UserResource: Login'); + + // Make sure the session exists in the database. + $result = db_query("SELECT * FROM {sessions} WHERE :uid=uid", array(':uid' => $account->uid))->fetchObject(); + $this->assertTrue(!empty($result), 'Session found', 'UserResource: Login'); + + // Save session details. + $this->session_id = $response_data->sessid; + $this->session_name = $response_data->session_name; + $this->loggedInUser = $response_data->user; + + // Try to login with another user to get error. + $account2 = $this->drupalCreateUser(); + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account2->name, 'password' => $account2->pass_raw)); + $this->assertTrue(strpos($response['status'], 'Already logged in as ' . $account->name) !== FALSE, + 'Session is properly opened for logged in user.', 'UserResource: Login'); + + // Logout. + $this->drupalLogout(); + + // Try to login with wrong credentials. + $response = $this->servicesPost($this->endpoint->path . '/user/login', + array('username' => $account->name, 'password' => $this->randomString())); + $this->assertTrue(strpos($response['status'], 'Wrong username or password') !== FALSE, + 'User cannot login with wrong username / password.', 'UserResource: Login'); + } + /** + * Test login method. API VERsion 1.1 + * + * Create user. Login. Try to login with another user (to get error). + * Login with wrong credentials (to get error). + */ + function testUserLoginMethodAPI_1_1() { + $this->endpoint = $this->saveNewVersionEndpoint('1.1'); + $path = $this->endpoint->path; + $account = $this->drupalCreateUser(); + + // Logout first. + $this->drupalLogout(); + + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $account->pass_raw)); + + $response_data = $response['body']; + $this->assertTrue(strpos($response['status'], 'Missing required argument name') !== FALSE, + 'User Resource is rejecting old parameter names.', 'UserResource: Login'); + + $responseArray = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $account->pass_raw), + array('services_user_login_version: 1.0')); + $this->assertTrue($responseArray['code'] == '200', 'Arguments should be old arguments and we should be logged in.', + 'Services Version System'); + $response_data = $responseArray['body']; + $proper_answer = isset($response_data->sessid) + && isset($response_data->user) + && $response_data->user->name == $account->name; + $this->assertTrue($proper_answer, 'User successfully logged in.', 'UserResource: Login'); + $this->drupalLogout(); + $responseArray = $this->servicesPost($this->endpoint->path . '/user/login', array('name' => $account->name, 'pass' => $account->pass_raw), + array('services_user_login_version: 1.1')); + $this->assertTrue($responseArray['code'] == '200', 'Arguments should be old arguments and we should be logged in.', + 'Services Version System'); + $response_data = $responseArray['body']; + $proper_answer = isset($response_data->sessid) + && isset($response_data->user) + && $response_data->user->name == $account->name; + $this->assertTrue($proper_answer, 'User successfully logged in.', 'UserResource: Login'); + } + /** + * Test flood control during user login + * + * Account blocking: Create user. Try to login with wrong credentials (get default error). + * Try to login fifth time and get account blocking error. + * + * IP blocking: Create set of users to provide 50 failed attempts to login (less then 5 to prevent account blocking) + * and get IP blocking error + */ + function testUserLoginFloodControl() { + $account = $this->drupalCreateUser(); + + // Logout first + $this->drupalLogout(); + + // First failed login (wrong password) + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $this->randomString())); + + // Get default wrong credentials error + $this->assertTrue(strpos($response['status'], 'Wrong username or password') !== FALSE, + 'User cannot login with wrong username / password.', 'UserResource: Login'); + + $account_blocking_limit = variable_get('user_failed_login_user_limit', 5); + + // Go through set of default error while we're having attempts + if ($account_blocking_limit > 2) { + for ($i = 0; $i < $account_blocking_limit - 2; $i++) { + // Just trigger login operation to write fails to flood table + $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $this->randomString())); + } + } + + // Now account will be locked after 5 failed attempts + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account->name, 'password' => $this->randomString())); + + $this->assertTrue(strpos($response['status'], 'Account is temporarily blocked.') !== FALSE, + 'After ' . $account_blocking_limit . '-th failed login account is temporary blocked.', 'UserResource: Login Flood Control'); + + // Test IP blocking + $ip_blocking_limit = variable_get('user_failed_login_ip_limit', 50); + $account2 = $this->drupalCreateUser(); + + // Provide necessary count of test users to get 50 failed attempts without account blocking + for ($i = 0; $i < $ip_blocking_limit - $account_blocking_limit - 1; $i++) { + if ($i % $account_blocking_limit === 0) { + $account2 = $this->drupalCreateUser(); + } + + $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account2->name, 'password' => $this->randomString())); + } + + $account2 = $this->drupalCreateUser(); + + // Now ip will be locked after 50 failed attempts + $response = $this->servicesPost($this->endpoint->path . '/user/login', array('username' => $account2->name, 'password' => $this->randomString())); + + $this->assertTrue(strpos($response['status'], 'This IP address is temporarily blocked.') !== FALSE, + 'After ' . $ip_blocking_limit . '-th failed login ip is temporary blocked.', 'UserResource: Login Flood Control'); + } + + /** + * Test logout method. + */ + function testUserLogout() { + // Logout via REST call. + $response = $this->servicesPost($this->endpoint->path . '/user/logout'); + // Try logout second time. + $this->drupalGet('user/logout'); + $this->assertText('You are not authorized to access this page', 'User logout successfully.', 'UserResource: Logout'); + // Login again. + $this->drupalLogin($this->privileged_user); + // Logout via REST call. + $response = $this->servicesPost($this->endpoint->path . '/user/logout'); + // Try to logout second time via REST call. + $response = $this->servicesPost($this->endpoint->path . '/user/logout'); + $this->assertTrue(strpos($response['status'], 'User is not logged in'), + 'User cannot logout when is anonymous', 'UserResource: Logout'); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesSecurityTests.test b/openthess/modules/services/tests/functional/ServicesSecurityTests.test new file mode 100644 index 0000000..b28143a --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesSecurityTests.test @@ -0,0 +1,100 @@ +endpoint = $this->saveNewEndpoint(); + + // Create and log in our privileged user. + $this->privileged_user = $this->drupalCreateUser(array('get a system variable', 'set a system variable')); + $this->drupalLogin($this->privileged_user); + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => t('Security'), + 'description' => t('Security tests.'), + 'group' => t('Services'), + ); + } + + public function testSessionCSRF() { + $variable_name = $this->randomName(); + $variable_value = $this->randomString(); + $default_variable_value = $this->randomString(); + $this->servicesPost($this->endpoint->path . '/system/set_variable', array('name' => $variable_name, 'value' => $variable_value)); + + $get_variable_args = array('name' => $variable_name, 'default' => $default_variable_value); + $response = $this->servicesPostNoCSRFHeader($this->endpoint->path . '/system/get_variable', $get_variable_args); + $this->assertEqual($response['status'], 'HTTP/1.1 401 Unauthorized: CSRF validation failed'); + + $bad_csrf_token_headers = array('X-CSRF-Token: ' . $this->randomString()); + $response = $this->servicesPostNoCSRFHeader($this->endpoint->path . '/system/get_variable', $get_variable_args, $bad_csrf_token_headers); + $this->assertEqual($response['status'], 'HTTP/1.1 401 Unauthorized: CSRF validation failed'); + + $csrf_token = $this->drupalGet('services/session/token'); + $good_csrf_token_headers = array('X-CSRF-Token: ' . $csrf_token); + $response = $this->servicesPostNoCSRFHeader($this->endpoint->path . '/system/get_variable', $get_variable_args, $good_csrf_token_headers); + $this->assertEqual($response['body'], $variable_value, 'Value of variable retrieved.'); + } + + /** + * Copy of servicesPost method but without CSRF header. + */ + protected function servicesPostNoCSRFHeader($url, $data = array(), $headers = array(), $call_type = 'php') { + switch ($call_type) { + case 'php': + // Add .php to get serialized response. + $url = $this->getAbsoluteUrl($url) . '.php'; + // Otherwise Services will reject arguments. + $headers[] = "Content-type: application/x-www-form-urlencoded"; + // Prepare arguments. + $post = drupal_http_build_query($data, '', '&'); + break; + case 'json': + // Add .json to get json encoded response. + $url = $this->getAbsoluteUrl($url) . '.json'; + // Set proper headers. + $headers[] = "Content-type: application/json"; + // Prepare arguments. + $post = json_encode($data); + break; + } + + $content = $this->curlExec(array( + CURLOPT_URL => $url, + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => $post, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADER => TRUE, + CURLOPT_RETURNTRANSFER => TRUE + )); + + // Parse response. + list($info, $header, $status, $code, $body) = $this->parseHeader($content, $call_type); + + $this->verbose('POST request to: ' . $url . + '
Arguments: ' . highlight_string('Raw POST body: ' . $post . + '
Response: ' . highlight_string('Curl info: ' . highlight_string('Raw response: ' . $content); + return array('header' => $header, 'status' => $status, 'code' => $code, 'body' => $body); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesVersionTests.test b/openthess/modules/services/tests/functional/ServicesVersionTests.test new file mode 100644 index 0000000..9a1b5d9 --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesVersionTests.test @@ -0,0 +1,109 @@ +privileged_user = $this->drupalCreateUser(array('administer services',)); + $this->drupalLogin($this->privileged_user); + } + + /** + * Implementation of getInfo(). + */ + public static function getInfo() { + return array( + 'name' => 'Version System', + 'description' => 'Test the Version system', + 'group' => 'Services', + ); + } + + /** + * Test connect method. + */ + function testVersion() { + // Set up endpoint. + $this->endpoint = $this->saveNewVersionEndpoint('1.0'); + $path = $this->endpoint->path; + $updates = services_get_updates(); + if (is_array($updates)) { + foreach ($updates as $key => $update) { + foreach ($update as $resource_key => $updates) { + $versions = services_get_update_versions($key, $resource_key); + if(count($versions)) { + $this->pass('Detected multiple versions for a resource', 'Services Version System'); + } else { + $this->fail('Failed to detect any versions for our test resource.', 'Services Version System'); + } + + } + } + } else { + $this->fail('Failed to get services updates', 'Services Version System'); + } + + $responseArray = $this->servicesGet($this->endpoint->path . '/services_test/AjSHa'); + //In version 1.0 of services_test resource theres only 1 argument + $this->assertTrue('CRUD Retrieve AjSHa' == $responseArray['body'], 'Successfully received sent param on version 1.0 api'); + $this->endpoint = $this->saveNewVersionEndpoint('1.0'); + //Test the ability to say I want 1.1 api by passing in the header. + $responseArray = $this->servicesGet($this->endpoint->path . '/services_test/AjSHa', NULL, + array('services_services_test_retrieve_version: 1.1')); + $this->assertTrue($responseArray['code'] == '401', 'Successfully was rejected hopefully more missing parameters', + 'Services Version System'); + $this->assertTrue(strpos($responseArray['status'], 'Missing required argument arg2'), + 'Yay, looks like were missing a required argument', 'Services Version System'); + + $responseArray = $this->servicesGet($this->endpoint->path . '/services_test/AjSHa', NULL, + array('services_services_test_retrieve_version: 1.2')); + $this->assertTrue($responseArray['code'] == '200', 'Argument two should be optional now. Looks like it was.', + 'Services Version System'); + $this->assertTrue($responseArray['body'] == 'AjSHa:0', 'Our response looks good, and its a default argument', + 'Services Version System'); + $responseArray = $this->servicesGet($this->endpoint->path . '/services_test/AjSHa', array('arg2' => 'test'), + array('services_services_test_retrieve_version: 1.2')); + $this->assertTrue($responseArray['code'] == '200', + 'Argument two should be optional now. Looks like it was.', 'Services Version System'); + $this->assertTrue($responseArray['body'] == 'AjSHa:test', + 'Our response looks good, and its our passed arguments', 'Services Version System'); + + $this->endpoint = $this->saveNewVersionEndpoint('1.1'); + //Test the ability to say I want 1.1 api by passing in the header. + $responseArray = $this->servicesGet($this->endpoint->path . '/services_test/AjSHa'); + $this->assertTrue($responseArray['code'] == '401', + 'Successfully was rejected hopefully more missing parameters', 'Services Version System'); + $this->assertTrue(strpos($responseArray['status'], 'Missing required argument arg2'), + 'Yay, looks like were missing a required argument', 'Services Version System'); + $this->endpoint = $this->saveNewVersionEndpoint('1.2'); + $responseArray = $this->servicesGet($this->endpoint->path . '/services_test/AjSHa'); + $this->assertTrue($responseArray['code'] == '200', + 'Argument two should be optional now. Looks like it was.', 'Services Version System'); + $this->assertTrue($responseArray['body'] == 'AjSHa:0', 'Our response looks good, and its a default argument', + 'Services Version System'); + $responseArray = $this->servicesGet($this->endpoint->path . '/services_test/AjSHa', array('arg2' => 'test')); + $this->assertTrue($responseArray['code'] == '200', + 'Argument two should be optional now. Looks like it was.', 'Services Version System'); + $this->assertTrue($responseArray['body'] == 'AjSHa:test', + 'Our response looks good, and its our passed arguments', 'Services Version System'); + } +} diff --git a/openthess/modules/services/tests/functional/ServicesXMLRPCTests.test b/openthess/modules/services/tests/functional/ServicesXMLRPCTests.test new file mode 100644 index 0000000..314b98f --- /dev/null +++ b/openthess/modules/services/tests/functional/ServicesXMLRPCTests.test @@ -0,0 +1,235 @@ + 'XMLRPC Server', + 'description' => 'Test XMLRPC server.', + 'group' => 'Services', + ); + } + + public function setUp() { + parent::setUp('ctools', 'services', 'xmlrpc_server', 'services_test_resource'); + // Set up endpoint. + $this->endpoint = $this->saveNewEndpoint(); + } + + /** + * Test list.Methods call. + * + * Regression http://drupal.org/node/1072844. + */ + function testlistMethods() { + $result = $this->servicesXMLRPC('system.listMethods', array()); + $this->assertTrue(in_array('node.index', $result['body']), 'node.index method found.', 'XMLRPC: listMethods'); + } + + /** + * Test user login. + */ + function testUserLogin() { + // Create user. + $user = $this->drupalCreateUser(array('access user profiles')); + $args = array( + 'username' => $user->name, + 'password' => $user->pass_raw, + ); + $result = $this->servicesXMLRPC('user.login', $args); + $this->assertEqual($result['body']['user']['uid'], $user->uid, + format_string('User %user logged in successfully.', array('%user' => $user->name)), 'XMLRPC: UserLogin'); + + $this->sessid = $result['body']['sessid']; + $this->session_name = $result['body']['session_name']; + + // Call index method as logged in user. + $args = array( + 'page' => 0, + 'fields' => '*', + 'parameters' => array(), + ); + $result = $this->servicesXMLRPC('user.index', $args); + // There should be three users available: anonymous, admin and newly created. + $this->assertTrue(count($result['body']) == 3, 'Users listed properly.', 'XMLRPC: UserLogin'); + } + + /** + * Precedence CRUD methods > Actions > Relations > Targeted Actions + * + * @see http://drupal.org/node/1016350 + */ + function testPrecedence() { + $args = array('arg1' => $this->randomName()); + $result = $this->servicesXMLRPC('services_test.retrieve', $args); + $this->assertEqual($result['body'], 'CRUD Retrieve ' . $args['arg1'], + 'XMLRPC precedence works properly (CRUD higher priority than action).', 'XMLRPC: Precedence'); + } + + public function saveNewEndpoint() { + $edit = $this->populateEndpointFAPI() ; + $endpoint = new stdClass; + $endpoint->disabled = FALSE; /* Edit this to true to make a default endpoint disabled initially */ + $endpoint->api_version = 3; + $endpoint->name = $edit['name']; + $endpoint->title = $edit['title']; + $endpoint->server = $edit['server']; + $endpoint->path = $edit['path']; + $endpoint->authentication = array( + 'services' => 'services', + ); + $endpoint->server_settings = array( + 'formatters' => array( + 'json' => TRUE, + 'bencode' => TRUE, + 'rss' => TRUE, + 'plist' => TRUE, + 'xmlplist' => TRUE, + 'php' => TRUE, + 'yaml' => TRUE, + 'jsonp' => FALSE, + 'xml' => FALSE, + ), + 'parsers' => array( + 'application/x-yaml' => TRUE, + 'application/json' => TRUE, + 'application/vnd.php.serialized' => TRUE, + 'application/plist' => TRUE, + 'application/plist+xml' => TRUE, + 'application/x-www-form-urlencoded' => TRUE, + ), + ); + $endpoint->resources = array( + 'system' => array( + 'alias' => '', + 'actions' => array( + 'connect' => array( + 'enabled' => 1, + ), + 'get_variable' => array( + 'enabled' => 1, + ), + 'set_variable' => array( + 'enabled' => 1, + ), + ), + ), + 'user' => array( + 'alias' => '', + 'operations' => array( + 'create' => array( + 'enabled' => 1, + ), + 'retrieve' => array( + 'enabled' => 1, + ), + 'update' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'login' => array( + 'enabled' => 1, + ), + 'logout' => array( + 'enabled' => 1, + ), + ), + ), + 'services_test' => array( + 'alias' => '', + 'operations' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + ), + ), + ); + $endpoint->debug = 1; + $endpoint->export_type = FALSE; + services_endpoint_save($endpoint); + $endpoint = services_endpoint_load($endpoint->name); + $this->assertTrue($endpoint->name == $edit['name'], 'Endpoint successfully created'); + return $endpoint; + } + + public function populateEndpointFAPI() { + return array( + 'name' => 'machinename', + 'title' => $this->randomName(20), + 'path' => $this->randomName(10), + 'server' => 'xmlrpc_server', + ); + } + + /** + * Do XMLRPC call. + * + * @param string $method + * Name of method to call. + * @param array $args + * Arguments to pass to call. + * @param bool $sessid + * Add cookies in order to log in. + * @param bool $assert_no_error + * Whether assert that no error returned. + * @return array + * array( + * 'body' -- answer of call + * 'error_message' -- error message if any + * ) + */ + public function servicesXMLRPC($method, $args = array(), $sessid = TRUE, $assert_no_error = TRUE) { + if (!is_array($args)) { + $args = array($args); + } + + $options = array('headers' => array()); + + // Set up cookies. + if ($sessid && !empty($this->sessid)) { + $options['headers']['Cookie'] = $this->session_name . '=' . $this->sessid; + } + + $csrf_token_response = xmlrpc(url($this->endpoint->path, array('absolute' => TRUE)), array('user.token' => array()), $options); + $options['headers']['X-CSRF-Token'] = $csrf_token_response['token']; + + $output = xmlrpc(url($this->endpoint->path, array('absolute' => TRUE)), array($method => $args), $options); + + $error_message = xmlrpc_error_msg(); + + if ($assert_no_error) { + $this->assertTrue(empty($error_message), format_string('XMLRPC call %method run without errors.', array('%method' => $method)), 'XMLRPC call'); + } + $this->verbose('XMLRPC request to: ' . $method . + '
Arguments: ' . highlight_string('Response: ' . highlight_string('Error: ' . $error_message); + + if (!empty($error_message)) { + return array('error_message' => $error_message, 'body' => ''); + } + + return array('error_message' => '', 'body' => $output); + } +} diff --git a/openthess/modules/services/tests/services.test b/openthess/modules/services/tests/services.test new file mode 100644 index 0000000..ee5131e --- /dev/null +++ b/openthess/modules/services/tests/services.test @@ -0,0 +1,886 @@ +cookieFile = drupal_tempnam(variable_get('file_temporary_path'), 'services_cookiejar'); + // Load the cookie file when initializing Curl. + $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile; + } + + /** + * Perform GET request. + */ + protected function servicesGet($url, $data = NULL, $headers = array()) { + $options = array('query' => $data); + $url = url($this->getAbsoluteUrl($url) . '.php', $options); + $content = $this->curlExec(array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_URL => $url, + CURLOPT_NOBODY => FALSE, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HEADER => TRUE, + CURLOPT_HTTPHEADER => $headers + )); + + // Parse response. + list($info, $header, $status, $code, $body) = $this->parseHeader($content); + + $this->verbose('GET request to: ' . $url . + '
headers: ' . highlight_string('Arguments: ' . highlight_string('Response: ' . highlight_string('Raw response: ' . $content); + return array('header' => $header, 'status' => $status, 'code' => $code, 'body' => $body); + } + + /** + * Post file as multipart/form-data. + */ + protected function servicesPostFile($url, $filepath, $headers = array(), $additional_arguments = array()) { + $this->addCSRFHeader($headers); + if (!is_array($filepath)) { + $filepath = array($filepath); + } + // Add .php to get serialized response. + $url = $this->getAbsoluteUrl($url) . '.php'; + + // Otherwise Services will reject arguments. + $headers[] = "Content-type: multipart/form-data"; + // Prepare arguments. + $post = $additional_arguments; + $i = 0; + foreach ($filepath as $path) { + $post['files[file_contents' . $i . ']'] = '@' . variable_get('file_public_path', '') . '/' . file_uri_target($path); + $i++; + } + + $content = $this->curlExec(array( + CURLOPT_URL => $url, + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => $post, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADER => TRUE, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_FOLLOWLOCATION => TRUE, + CURLOPT_VERBOSE => TRUE, + )); + + // Parse response. + list($info, $header, $status, $code, $body) = $this->parseHeader($content); + + $this->verbose('POST request to: ' . $url . + '
File Name(s): ' . highlight_string('Response: ' . highlight_string('Curl info: ' . highlight_string('Raw response: ' . $content); + return array('header' => $header, 'status' => $status, 'code' => $code, 'body' => $body); + } + + /** + * Perform POST request. + */ + protected function servicesPost($url, $data = array(), $headers = array(), $call_type = 'php') { + $this->addCSRFHeader($headers); + + switch ($call_type) { + case 'php': + // Add .php to get serialized response. + $url = $this->getAbsoluteUrl($url) . '.php'; + // Otherwise Services will reject arguments. + $headers[] = "Content-type: application/x-www-form-urlencoded"; + // Prepare arguments. + $post = drupal_http_build_query($data, '', '&'); + break; + case 'json': + // Add .json to get json encoded response. + $url = $this->getAbsoluteUrl($url) . '.json'; + // Set proper headers. + $headers[] = "Content-type: application/json"; + // Prepare arguments. + $post = json_encode($data); + break; + } + + $content = $this->curlExec(array( + CURLOPT_URL => $url, + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => $post, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADER => TRUE, + CURLOPT_RETURNTRANSFER => TRUE + )); + + // Parse response. + list($info, $header, $status, $code, $body) = $this->parseHeader($content, $call_type); + + $this->verbose('POST request to: ' . $url . + '
Arguments: ' . highlight_string('Raw POST body: ' . $post . + '
Response: ' . highlight_string('Curl info: ' . highlight_string('Raw response: ' . $content); + return array('header' => $header, 'status' => $status, 'code' => $code, 'body' => $body); + } + + /** + * Perform PUT request. + */ + protected function servicesPut($url, $data = NULL, $headers = array(), $call_type = 'php') { + $this->addCSRFHeader($headers); + switch ($call_type) { + case 'php': + // Add .php to get serialized response. + $url = $this->getAbsoluteUrl($url) . '.php'; + // Otherwise Services will reject arguments. + $headers[] = "Content-type: application/x-www-form-urlencoded"; + // Prepare arguments. + $post = drupal_http_build_query($data, '', '&'); + break; + case 'json': + // Add .json to get json encoded response. + $url = $this->getAbsoluteUrl($url) . '.json'; + // Set proper headers. + $headers[] = "Content-type: application/json"; + // Prepare arguments. + $post = json_encode($data); + break; + } + + // Emulate file. + $putData = fopen('php://temp', 'rw+'); + fwrite($putData, $post); + fseek($putData, 0); + + $content = $this->curlExec(array( + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_PUT => TRUE, + CURLOPT_HEADER => TRUE, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_INFILE => $putData, + CURLOPT_INFILESIZE => drupal_strlen($post) + )); + fclose($putData); + + // Parse response. + list($info, $header, $status, $code, $body) = $this->parseHeader($content, $call_type); + + $this->verbose('PUT request to: ' . $url . + '
Arguments: ' . highlight_string('Raw POST body: ' . $post . + '
Response: ' . highlight_string('Curl info: ' . highlight_string('Raw response: ' . $content); + return array('header' => $header, 'status' => $status, 'code' => $code, 'body' => $body); + } + + /** + * Perform DELETE request. + */ + protected function servicesDelete($url, $data = NULL, $headers = array()) { + $this->addCSRFHeader($headers); + $options = array('query' => $data); + $url = url($this->getAbsoluteUrl($url) . '.php', $options); + + $content = $this->curlExec(array( + CURLOPT_URL => $url, + CURLOPT_CUSTOMREQUEST => "DELETE", + CURLOPT_HEADER => TRUE, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_RETURNTRANSFER => TRUE + )); + + // Parse response. + list($info, $header, $status, $code, $body) = $this->parseHeader($content); + + $this->verbose('DELETE request to: ' . $url . + '
Arguments: ' . highlight_string('Response: ' . highlight_string('Curl info: ' . highlight_string('Raw response: ' . $content); + return array('header' => $header, 'status' => $status, 'code' => $code, 'body' => $body); + } + + /** + * Perform HEAD request. + */ + protected function servicesHead($url) { + $url = url($this->getAbsoluteUrl($url) . '.php'); + + $content = $this->curlExec(array( + CURLOPT_URL => $url, + CURLOPT_CUSTOMREQUEST => 'HEAD', + CURLOPT_HEADER => TRUE, + CURLOPT_RETURNTRANSFER => TRUE + )); + + // Parse response. + list($info, $header, $status, $code, $body) = $this->parseHeader($content); + + $this->verbose('HEAD request to: ' . $url . + '
Curl info: ' . highlight_string('Raw response: ' . $content); + return array('header' => $header, 'status' => $status, 'code' => $code, 'body' => $body); + } + + /* + ------------------------------------ + HELPER METHODS + ------------------------------------ + */ + + /** + * Parse header. + * + * @param type $content + * @return type + */ + function parseHeader($content, $call_type = 'php') { + $info = curl_getinfo($this->curlHandle); + $header = drupal_substr($content, 0, $info['header_size']); + $header = str_replace("HTTP/1.1 100 Continue\r\n\r\n", '', $header); + $status = strtok($header, "\r\n"); + $code = $info['http_code']; + + $raw_body = drupal_substr($content, $info['header_size'], drupal_strlen($content) - $info['header_size']); + switch ($call_type) { + case 'php': + $body = unserialize($raw_body); + break; + case 'json': + $body = json_decode($raw_body); + break; + } + return array($info, $header, $status, $code, $body); + } + + /** + * Retrieve and set CSFR token header. + * + * @param array $headers + */ + function addCSRFHeader(&$headers) { + $csrf_token = $this->drupalGet('services/session/token'); + $headers[] = 'X-CSRF-Token: ' . $csrf_token; + } + + /** + * Creates a data array for populating an endpoint creation form. + * + * @return + * An array of fields for fully populating an endpoint creation form. + */ + public function populateEndpointFAPI() { + return array( + 'name' => strtolower($this->randomName(10)), + 'path' => $this->randomName(10), + 'server' => 'rest_server', + ); + } + + public function saveNewEndpoint() { + $edit = $this->populateEndpointFAPI() ; + $endpoint = new stdClass; + $endpoint->disabled = FALSE; /* Edit this to true to make a default endpoint disabled initially */ + $endpoint->api_version = 3; + $endpoint->name = $edit['name']; + $endpoint->server = $edit['server']; + $endpoint->path = $edit['path']; + $endpoint->authentication = array( + 'services' => 'services', + ); + $endpoint->server_settings = array( + 'formatters' => array( + 'json' => TRUE, + 'bencode' => TRUE, + 'rss' => TRUE, + 'plist' => TRUE, + 'xmlplist' => TRUE, + 'php' => TRUE, + 'yaml' => TRUE, + 'jsonp' => FALSE, + 'xml' => FALSE, + ), + 'parsers' => array( + 'application/x-yaml' => TRUE, + 'application/json' => TRUE, + 'application/vnd.php.serialized' => TRUE, + 'application/plist' => TRUE, + 'application/plist+xml' => TRUE, + 'application/x-www-form-urlencoded' => TRUE, + 'multipart/form-data' => TRUE, + ), + ); + $endpoint->resources = array( + 'comment' => array( + 'operations' => array( + 'create' => array( + 'enabled' => 1, + ), + 'retrieve' => array( + 'enabled' => 1, + ), + 'update' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'countAll' => array( + 'enabled' => 1, + ), + 'countNew' => array( + 'enabled' => 1, + ), + ), + ), + 'file' => array( + 'operations' => array( + 'create' => array( + 'enabled' => 1, + ), + 'retrieve' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'create_raw' => array( + 'enabled' => 1, + ), + ), + ), + 'node' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + 'create' => array( + 'enabled' => 1, + ), + 'update' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'relationships' => array( + 'files' => array( + 'enabled' => 1, + ), + 'comments' => array( + 'enabled' => 1, + ), + ), + 'targeted_actions' => array( + 'attach_file' => array( + 'enabled' => 1, + ), + ), + ), + 'system' => array( + 'actions' => array( + 'connect' => array( + 'enabled' => 1, + ), + 'get_variable' => array( + 'enabled' => 1, + ), + 'set_variable' => array( + 'enabled' => 1, + ), + 'del_variable' => array( + 'enabled' => 1, + ), + ), + ), + 'taxonomy_term' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + 'create' => array( + 'enabled' => 1, + ), + 'update' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'selectNodes' => array( + 'enabled' => 1, + ), + ), + ), + 'taxonomy_vocabulary' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + 'create' => array( + 'enabled' => 1, + ), + 'update' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'getTree' => array( + 'enabled' => 1, + ), + ), + ), + 'user' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => 1, + ), + 'create' => array( + 'enabled' => 1, + ), + 'update' => array( + 'enabled' => 1, + ), + 'delete' => array( + 'enabled' => 1, + ), + 'index' => array( + 'enabled' => 1, + ), + ), + 'actions' => array( + 'login' => array( + 'enabled' => 1, + ), + 'logout' => array( + 'enabled' => '1', + 'settings' => array( + 'services' => array( + 'resource_api_version' => '1.1', + ), + ), + ), + 'register' => array( + 'enabled' => 1, + ), + ), + 'targeted_actions' => array( + 'cancel' => array( + 'enabled' => 1, + ), + 'password_reset' => array( + 'enabled' => 1, + ), + 'resend_welcome_email' => array( + 'enabled' => 1, + ), + ), + ), + ); + $endpoint->debug = 1; + $endpoint->export_type = FALSE; + services_endpoint_save($endpoint); + $endpoint = services_endpoint_load($endpoint->name); + $this->assertTrue($endpoint->name == $edit['name'], 'Endpoint successfully created'); + return $endpoint; + } + + public function saveNewVersionEndpoint($version = '1.0') { + $edit = $this->populateEndpointFAPI() ; + $endpoint = new stdClass(); + $endpoint->disabled = FALSE; /* Edit this to true to make a default endpoint disabled initially */ + $endpoint->api_version = 3; + $endpoint->name = $edit['name']; + $endpoint->server = $edit['server']; + $endpoint->path = $edit['path']; + $endpoint->authentication = array( + 'services' => array(), + ); + $endpoint->server_settings = array( + 'formatters' => array( + 'bencode' => TRUE, + 'json' => TRUE, + 'php' => TRUE, + 'plist' => TRUE, + 'rss' => TRUE, + 'xml' => TRUE, + 'xmlplist' => TRUE, + 'jsonp' => FALSE, + ), + 'parsers' => array( + 'application/json' => TRUE, + 'application/plist' => TRUE, + 'application/plist+xml' => TRUE, + 'application/vnd.php.serialized' => TRUE, + 'multipart/form-data' => TRUE, + 'application/x-www-form-urlencoded' => TRUE, + ), + ); + $endpoint->resources = array( + 'comment' => array( + 'operations' => array( + 'create' => array( + 'enabled' => '1', + ), + 'retrieve' => array( + 'enabled' => '1', + ), + 'update' => array( + 'enabled' => '1', + ), + 'delete' => array( + 'enabled' => '1', + ), + 'index' => array( + 'enabled' => '1', + ), + ), + 'actions' => array( + 'countAll' => array( + 'enabled' => '1', + ), + 'countNew' => array( + 'enabled' => '1', + ), + ), + ), + 'file' => array( + 'operations' => array( + 'create' => array( + 'enabled' => '1', + ), + 'retrieve' => array( + 'enabled' => '1', + ), + 'delete' => array( + 'enabled' => '1', + ), + 'index' => array( + 'enabled' => '1', + ), + ), + 'actions' => array( + 'create_raw' => array( + 'enabled' => '1', + ), + ), + ), + 'node' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => '1', + ), + 'create' => array( + 'enabled' => '1', + ), + 'update' => array( + 'enabled' => '1', + ), + 'delete' => array( + 'enabled' => '1', + ), + 'index' => array( + 'enabled' => '1', + ), + ), + 'relationships' => array( + 'files' => array( + 'enabled' => '1', + ), + 'comments' => array( + 'enabled' => '1', + ), + ), + ), + 'system' => array( + 'actions' => array( + 'connect' => array( + 'enabled' => '1', + ), + 'get_variable' => array( + 'enabled' => '1', + ), + 'set_variable' => array( + 'enabled' => '1', + ), + 'del_variable' => array( + 'enabled' => '1', + ), + ), + ), + 'taxonomy_term' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => '1', + ), + 'create' => array( + 'enabled' => '1', + ), + 'update' => array( + 'enabled' => '1', + ), + 'delete' => array( + 'enabled' => '1', + ), + 'index' => array( + 'enabled' => '1', + ), + ), + 'actions' => array( + 'selectNodes' => array( + 'enabled' => '1', + ), + ), + ), + 'taxonomy_vocabulary' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => '1', + ), + 'create' => array( + 'enabled' => '1', + ), + 'update' => array( + 'enabled' => '1', + ), + 'delete' => array( + 'enabled' => '1', + ), + 'index' => array( + 'enabled' => '1', + ), + ), + 'actions' => array( + 'getTree' => array( + 'enabled' => '1', + ), + ), + ), + 'user' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => '1', + ), + 'create' => array( + 'enabled' => '1', + ), + 'update' => array( + 'enabled' => '1', + ), + 'delete' => array( + 'enabled' => '1', + ), + 'index' => array( + 'enabled' => '1', + ), + ), + 'actions' => array( + 'login' => array( + 'enabled' => '1', + 'settings' => array( + 'services' => array( + 'resource_api_version' => $version, + ), + ), + ), + 'logout' => array( + 'enabled' => '1', + ), + 'register' => array( + 'enabled' => '1', + ), + ), + 'targeted_actions' => array( + 'cancel' => array( + 'enabled' => 1, + ), + 'password_reset' => array( + 'enabled' => 1, + ), + 'resend_welcome_email' => array( + 'enabled' => 1, + ), + ), + ), + 'services_test' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => '1', + 'settings' => array( + 'services' => array( + 'resource_api_version' => $version, + ), + ), + ), + ), + ), + 'views' => array( + 'operations' => array( + 'retrieve' => array( + 'enabled' => '1', + ), + ), + ), + ); + $endpoint->debug = 0; + $endpoint->export_type = FALSE; + services_endpoint_save($endpoint); + $endpoint = services_endpoint_load($endpoint->name); + $this->assertTrue($endpoint->name == $edit['name'], 'Endpoint successfully created'); + return $endpoint; + } + + /** + * Performs a cURL exec with the specified options after calling curlConnect(). + * + * @param $curl_options + * Custom cURL options. + * @return + * Content returned from the exec. + */ + protected function curlExec($curl_options, $redirect = FALSE) { + // Some Curl options might leave the handle in a state where subsequent + // request can cause warnings or even weird failures, so to be on the safe + // side we reinitialize Curl for each request. + $this->curlClose(); + + $this->curlInitialize(); + + // cURL incorrectly handles URLs with a fragment by including the + // fragment in the request to the server, causing some web servers + // to reject the request citing "400 - Bad Request". To prevent + // this, we strip the fragment from the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { + $original_url = $curl_options[CURLOPT_URL]; + $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + } + + $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; + + if (!empty($curl_options[CURLOPT_POST])) { + // This is a fix for the Curl library to prevent Expect: 100-continue + // headers in POST requests, that may cause unexpected HTTP response + // codes from some webservers (like lighttpd that returns a 417 error + // code). It is done by setting an empty "Expect" header field that is + // not overwritten by Curl. + $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:'; + } + + curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); + + if (!$redirect) { + // Reset headers, the session ID and the redirect counter. + $this->session_id = NULL; + $this->headers = array(); + $this->redirect_count = 0; + } + + $content = curl_exec($this->curlHandle); + $status = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); + + // cURL incorrectly handles URLs with fragments, so instead of + // letting cURL handle redirects we take of them ourselves to + // to prevent fragments being sent to the web server as part + // of the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (in_array($status, array(300, 301, 302, 303, 305, 307)) && $this->redirect_count < variable_get('simpletest_maximum_redirects', 5)) { + if ($this->drupalGetHeader('location')) { + $this->redirect_count++; + $curl_options = array(); + $curl_options[CURLOPT_URL] = $this->drupalGetHeader('location'); + $curl_options[CURLOPT_HTTPGET] = TRUE; + return $this->curlExec($curl_options, TRUE); + } + } + + $this->drupalSetContent($content, isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL)); + + // Analyze the method for log message. + $method = ''; + if (!empty($curl_options[CURLOPT_NOBODY])) { + $method = 'HEAD'; + } + + if (empty($method) && !empty($curl_options[CURLOPT_PUT])) { + $method = 'PUT'; + } + + if (empty($method) && !empty($curl_options[CURLOPT_CUSTOMREQUEST])) { + $method = $curl_options[CURLOPT_CUSTOMREQUEST]; + } + + if (empty($method)) { + $method = empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'; + } + $message_vars = array( + '!method' => $method, + '@url' => isset($original_url) ? $original_url : $url, + '@status' => $status, + '!length' => format_size(drupal_strlen($this->drupalGetContent())) + ); + $message = format_string('!method @url returned @status (!length).', $message_vars); + $this->assertTrue($this->drupalGetContent() !== FALSE, $message, 'Browser'); + return $this->drupalGetContent(); + } + /** + * Default values of comment for creating. + */ + public function getCommentValues($nid) { + return array( + 'subject' => $this->randomString(), + 'comment_body' => array( + LANGUAGE_NONE => array( + array( + 'value' => $this->randomString(), + 'format' => filter_default_format(), + ) + ) + ), + 'name' => $this->privileged_user->name, + 'language' => LANGUAGE_NONE, + 'nid' => $nid, + 'uid' => $this->privileged_user->uid, + 'cid' => NULL, + 'pid' => 0, + ); + } +} diff --git a/openthess/modules/services/tests/services_test_resource/services_test_resource.info b/openthess/modules/services/tests/services_test_resource/services_test_resource.info new file mode 100644 index 0000000..d177e39 --- /dev/null +++ b/openthess/modules/services/tests/services_test_resource/services_test_resource.info @@ -0,0 +1,17 @@ +name = Services Test Resource +description = Provide test methods to check different situations. +package = Services +core = 7.x +php = 5.x + +; This module for tests only. +hidden = TRUE + +dependencies[] = services + +; Information added by Drupal.org packaging script on 2014-01-31 +version = "7.x-3.7" +core = "7.x" +project = "services" +datestamp = "1391207946" + diff --git a/openthess/modules/services/tests/services_test_resource/services_test_resource.module b/openthess/modules/services/tests/services_test_resource/services_test_resource.module new file mode 100644 index 0000000..6ae450f --- /dev/null +++ b/openthess/modules/services/tests/services_test_resource/services_test_resource.module @@ -0,0 +1,181 @@ + array( + 'retrieve' => array( + 'callback' => '_services_test_resource_retrieve', + 'args' => array( + array( + 'name' => 'arg1', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'Test argument 1.', + ), + ), + 'access callback' => '_services_test_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + ), + 'actions' => array( + 'action_retrieve' => array( + 'access callback' => '_services_test_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'callback' => '_services_test_resource_action_retrieve', + 'args' => array( + array( + 'name' => 'arg1', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'Test argument 1.', + ), + ), + ), + ), + 'targeted_actions' => array( + 'test' => array( + 'access callback' => '_services_test_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + 'callback' => '_services_test_resource_targeted_action_test', + 'args' => array( + array( + 'name' => 'arg1', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'int', + 'description' => 'Test argument 1.', + ), + ), + ), + ), + ), + 'services_arguments_test' => array( + 'retrieve' => array( + 'callback' => '_services_arguments_test_resource_retrieve', + 'args' => array( + array( + 'name' => 'arg1', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'string', + 'description' => 'Test argument 1.', + ), + array( + 'name' => 'string', + 'optional' => FALSE, + 'source' => array('path' => 1), + 'type' => 'int', + 'description' => 'Test argument 2.', + ), + array( + 'name' => 'string', + 'optional' => TRUE, + 'source' => array('path' => 2), + 'type' => 'int', + 'default value' => '0', + 'description' => 'Test argument 3.', + ), + ), + 'access callback' => '_services_test_resource_access', + 'access arguments' => array('view'), + 'access arguments append' => TRUE, + ), + ), + ); +} + +/** + * CRUD retrieve callback. + */ +function _services_test_resource_retrieve($arg1) { + return 'CRUD Retrieve ' . $arg1; +} + +//Change type to string. +//Add an additional argument +function _services_test_resource_retrieve_update_1_1() { + $new_set = array( + 'help' => 'retrieve an item for yourself', + 'args' => array( + array( + 'name' => 'arg1', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'string', + 'description' => 'Test argument 1.', + ), + array( + 'name' => 'arg2', + 'optional' => FALSE, + 'source' => array('param' => 'arg2'), + 'type' => 'string', + 'description' => 'Test argument 2.', + ), + ), + ); + return $new_set; +} +//Make arg 2 optional. +//Update callback so the argument can be returned/used in some fashion. +function _services_test_resource_retrieve_update_1_2() { + $new_set = array( + 'callback' => '_services_test_resource_retrieve_callback_1_2', + 'help' => 'retrieve an item for yourself', + 'args' => array( + array( + 'name' => 'arg1', + 'optional' => FALSE, + 'source' => array('path' => 0), + 'type' => 'string', + 'description' => 'Test argument 1.', + ), + array( + 'name' => 'arg2', + 'optional' => TRUE, + 'source' => array('param' => 'arg2'), + 'type' => 'string', + 'description' => 'Test argument 2.', + 'default value' => 0, + ), + ), + ); + return $new_set; +} +//Update to retrieve callback +function _services_test_resource_retrieve_callback_1_2($arg1, $arg2) { + return $arg1 .':'. $arg2; +} +/** + * Action retrieve callback. + */ +function _services_test_resource_action_retrieve($arg1) { + return 'Action retrieve' . $arg1; +} + + +/** + * Targeted Action test callback. + */ +function _services_test_resource_targeted_action_test($arg1) { + return 'Targeted Action test' . $arg1; +} +/** + * Access callback. + */ +function _services_test_resource_access($op) { + return TRUE; +} + +/** + * Retrieve method of services_arguments_test resource. + */ +function _services_arguments_test_resource_retrieve($arg1, $arg2, $arg3) { + return format_string('Services arguments test @arg1 @arg2 @arg3', array('@arg1' => $arg1, '@arg2' => $arg2, '@arg3' => $arg3)); +} diff --git a/openthess/modules/services/tests/ui/ServicesUITests.test b/openthess/modules/services/tests/ui/ServicesUITests.test new file mode 100644 index 0000000..2790233 --- /dev/null +++ b/openthess/modules/services/tests/ui/ServicesUITests.test @@ -0,0 +1,109 @@ + 'UI tests', + 'description' => 'Test of Services UI.', + 'group' => 'Services', + ); + } + + function setUp() { + parent::setUp(array('ctools', 'services', 'rest_server')); + $this->privilegedUser = $this->drupalCreateUser(array('administer services', 'administer site configuration')); + $this->drupalLogin($this->privilegedUser); + } + + function testEndpointMachineName() { + // Try to create endpoint with bad machine name. + $edit = array( + 'name' => 're st', + 'server' => 'rest_server', + 'path' => 'rest', + ); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->assertText('The endpoint name can only consist of lowercase letters, underscores, and numbers.', + 'It is not possible to create endpoint with bad machine name.'); + + // Create endpoint properly. + $edit = array( + 'name' => 'rest', + 'server' => 'rest_server', + 'path' => 'rest', + ); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->assertText('rest', 'Endpoint create successfully.'); + + // Try to create endpoint with same machine name. + $edit = array( + 'name' => 'rest', + 'server' => 'rest_server', + 'path' => 'rest1', + ); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->assertText('The machine-readable name is already in use. It must be unique.', + 'It is not possible to create endpoint with existing machine name.'); + + // Try to create endpoint with same path. + $edit = array( + 'name' => 'rest1', + 'server' => 'rest_server', + 'path' => 'rest', + ); + $this->drupalPost('admin/structure/services/add', $edit, 'Save'); + $this->assertText('Endpoint path must be unique.', 'It is not possible to create endpoint with existing path.'); + } + + /** + * Test that adding a menu endpoint creates an menu path for that item. + */ + public function testEndpointMenu() { + // Create the endpoint. + $endpoint_settings = array( + 'name' => 'machine_name', + 'path' => $this->randomName(10), + 'server' => 'rest_server', + ); + + $this->drupalPost('admin/structure/services/add', $endpoint_settings, 'Save'); + $this->assertResponse('200', 'Create Endpoint.'); + + // Enable node resource index method. + $resource_settings = array( + 'resources[node][operations][index][enabled]' => '1', + ); + $this->drupalPost('admin/structure/services/list/' . $endpoint_settings['name'] . '/resources', + $resource_settings, 'Save'); + $this->assertResponse('200', 'Node resource index method enabled successfully.'); + + // Check path. + $this->drupalGet($endpoint_settings['path'] . '/node'); + $this->assertResponse('200', 'Accessed endpoint menu path node index method.'); + + // After accessing node resource we got logged out. So we login again. + $this->drupalLogin($this->privilegedUser); + + // Check edit. + $this->drupalGet('admin/structure/services/list/' . $endpoint_settings['name'] + . '/edit'); + $this->assertResponse('200', 'Access endpoint edit path.') ; + + // Check export. + $this->drupalGet('admin/structure/services/list/' . $endpoint_settings['name'] + . '/export'); + $this->assertResponse('200', 'Access endpoint export path.') ; + + // Check delete. + $this->drupalGet('admin/structure/services/list/' . $endpoint_settings['name'] + . '/delete'); + $this->assertResponse('200', 'Access endpoint delete path.') ; + } +} \ No newline at end of file diff --git a/openthess/modules/services/tests/unit/ServicesSpycLibraryTests.test b/openthess/modules/services/tests/unit/ServicesSpycLibraryTests.test new file mode 100644 index 0000000..f0d62e0 --- /dev/null +++ b/openthess/modules/services/tests/unit/ServicesSpycLibraryTests.test @@ -0,0 +1,40 @@ + 'Spyc Library', + 'description' => 'Test if we can download Spyc library.', + 'group' => 'Services', + ); + } + + /** + * Testing whether link in make file is valid. + */ + public function testMakeFileLinkValid() { + $makefile_path = drupal_get_path('module', 'services') . '/services.make'; + $makefile_content = file_get_contents($makefile_path); + // libraries[spyc][download][url] = "https://raw.github.com/mustangostang/spyc/79f61969f63ee77e0d9460bc254a27a671b445f3/spyc.php" + $matches = array(); + preg_match('/libraries\[spyc\]\[download\]\[url\] = (.*)/', $makefile_content, $matches); + $spyc_library_url = $matches[1]; + $spyc_library_content = file_get_contents($spyc_library_url); + + $search_keywords = array('assertTrue($spyc_library_valid, 'Spyc library can be downloaded from make file.'); + } +}