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:
', 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:
+ *
';
+ 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.');
+ }
+}