// swad_API.c: SWAD web API provided to external plugins

/*
    SWAD (Shared Workspace At a Distance),
    is a web platform developed at the University of Granada (Spain),
    and used to support university teaching.

    This file is part of SWAD core.
    Copyright (C) 1999-2025 Antonio Caas Vargas

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
/*****************************************************************************/
/******* How to use the gSOAP toolkit to create a web-service server *********/
/*****************************************************************************/
/*
This code uses the gSOAP toolkit:
http://www.cs.fsu.edu/~engelen/soap.html
http://gsoap2.sourceforge.net/
Version 2.8.8 (19/02/2012)

To install the toolkit:
 1. Install g++ if not installed: yum -y install gcc-c++
 2. Install yacc if not installed: yum -y install byacc
 3. Install flex if not installed: yum -y install flex
 4. Install bison if not installed: yum -y install bison
 5. Install bison-devel if not installed: yum -y install bison-devel
 6. unzip gsoap_2.8.8.zip
 7. cd gsoap-2.8
 8. ./configure
 9. make
10. make install

Steps to generate swad.wsdl:
1. Go to directory soap
cd /home/acanas/swad/swad/soap

2. Inside soap, create a C header file with the web service. Example swad_web_service.h:
----------
// File: swad_web_service.h
//gsoap swad service name: swad
//gsoap swad service namespace: urn:swad
//gsoap swad service location: https://swad.ugr.es/
...
----------

3. Inside soap, execute soapcpp2 compiler:
soapcpp2 -c -S swad_web_service.h

4. Copy generated WSDL file (swad.wsdl) to public location (probably you must be root):
cp -f /home/acanas/swad/swad/soap/swad.wsdl /var/www/html/ws/

5. Put a '#include "soap/soapH.h"' line into the code of the web-service server (this file).

6. Put a '#include "soap/swad.nsmap"' into the code of the web-service server (this file).

7. Compile swad including the web service.
The makefile must include -lgsoap, and compile soapC.c and soapServer.c files generated by soapcpp2 in step 2
Example of Makefile:
---------------------
OBJS = list of swad object files
SOAPOBJS = soap/soapC.o soap/soapServer.o
CC = gcc

LIBS = -lmysqlclient -lz -L/usr/lib64/mysql -lm -lgsoap

CFLAGS = -Wall -O2 -s

swad: $(OBJS) $(SOAPOBJS)
	$(CC) $(CFLAGS) -o $@ $(OBJS) $(SOAPOBJS) $(LIBS)
	chmod a+x $@
---------------------

8. Copy CGI (swad) to the cgi directory:
cp -f /home/acanas/swad/swad/swad /var/www/cgi-bin/

*/
/*****************************************************************************/
/********************************* Headers ***********************************/
/*****************************************************************************/
// In Eclipse, add to include path /usr/include, /usr/local/include, /usr/include/i386-linux-gnu

#include <dirent.h>		// For scandir, etc.
#include <linux/limits.h>	// For PATH_MAX
#include <stddef.h>		// For NULL
#include <string.h>
#include <stdsoap2.h>
#include <sys/stat.h>		// For lstat

#include "soap/soapH.h"		// gSOAP header
#include "soap/swad.nsmap"	// Namespaces map used

#include "swad_account.h"
#include "swad_API.h"
#include "swad_API_database.h"
#include "swad_attendance_database.h"
#include "swad_autolink.h"
#include "swad_browser.h"
#include "swad_browser_database.h"
#include "swad_course_database.h"
#include "swad_database.h"
#include "swad_enrolment_database.h"
#include "swad_error.h"
#include "swad_forum.h"
#include "swad_game_database.h"
#include "swad_global.h"
#include "swad_group_database.h"
#include "swad_hierarchy.h"
#include "swad_hierarchy_type.h"
#include "swad_ID.h"
#include "swad_mail_database.h"
#include "swad_mark.h"
#include "swad_match.h"
#include "swad_match_database.h"
#include "swad_message_database.h"
#include "swad_nickname_database.h"
#include "swad_notice.h"
#include "swad_notice_database.h"
#include "swad_notification.h"
#include "swad_notification_database.h"
#include "swad_old_new.h"
#include "swad_password.h"
#include "swad_plugin_database.h"
#include "swad_question_database.h"
#include "swad_role.h"
#include "swad_role_database.h"
#include "swad_room_database.h"
#include "swad_search.h"
#include "swad_session_database.h"
#include "swad_setting_database.h"
#include "swad_tag_database.h"
#include "swad_test_config.h"
#include "swad_test_visibility.h"
#include "swad_tree.h"
#include "swad_user.h"
#include "swad_user_database.h"
#include "swad_www.h"
#include "swad_xml.h"

/*****************************************************************************/
/************** External global variables from others modules ****************/
/*****************************************************************************/

extern struct Globals Gbl;
extern const char Str_BIN_TO_BASE64URL[64 + 1];

/*****************************************************************************/
/***************************** Private constants *****************************/
/*****************************************************************************/

// Add new functions at the end
static const char *API_Functions[1 + API_NUM_FUNCTIONS] =
  {
   [API_unknown			] = "?",			//  0 ==> unknown function
   [API_loginBySessionKey	] = "loginBySession",		//  1
   [API_loginByUserPassword	] = "loginByUserPassword",	//  2 (deprecated)
   [API_loginByUserPasswordKey	] = "loginByUserPasswordKey",	//  3
   [API_getCourses		] = "getCourses",		//  4
   [API_getUsers		] = "getUsers",			//  5
   [API_getNotifications	] = "getNotifications",		//  6
   [API_getTestConfig		] = "getTestConfig",		//  7
   [API_getTests		] = "getTests",			//  8
   [API_sendMessage		] = "sendMessage",		//  9
   [API_sendNotice		] = "sendNotice",		// 10
   [API_getDirectoryTree	] = "getDirectoryTree",		// 11
   [API_getGroups		] = "getGroups",		// 12
   [API_getGroupTypes		] = "getGroupTypes",		// 13
   [API_sendMyGroups		] = "sendMyGroups",		// 14
   [API_getFile			] = "getFile",			// 15
   [API_markNotificationsAsRead	] = "markNotificationsAsRead",	// 16
   [API_getNewPassword		] = "getNewPassword",		// 17
   [API_getCourseInfo		] = "getCourseInfo",		// 18
   [API_getAttendanceEvents	] = "getAttendanceEvents",	// 19
   [API_sendAttendanceEvent	] = "sendAttendanceEvent",	// 20
   [API_getAttendanceUsers	] = "getAttendanceUsers",	// 21
   [API_sendAttendanceUsers	] = "sendAttendanceUsers",	// 22
   [API_createAccount		] = "createAccount",		// 23
   [API_getMarks		] = "getMarks",			// 24
   [API_getTrivialQuestion	] = "getTrivialQuestion",	// 25
   [API_findUsers		] = "findUsers",		// 26
   [API_removeAttendanceEvent	] = "removeAttendanceEvent",	// 27
   [API_getGames		] = "getGames",			// 28
   [API_getMatches		] = "getMatches",		// 29
   [API_getMatchStatus		] = "getMatchStatus",		// 30
   [API_answerMatchQuestion	] = "answerMatchQuestion",	// 31
   [API_getLocation		] = "getLocation",		// 32
   [API_sendMyLocation		] = "sendMyLocation",		// 33
   [API_getLastLocation		] = "getLastLocation",		// 34
   [API_getAvailableRoles	] = "getAvailableRoles",	// 35
  };

/* Web service roles (they do not match internal swad-core roles) */
#define API_NUM_ROLES 4
typedef enum
  {
   API_ROLE_UNKNOWN = 0,	// User not logged in (do not change this constant to any value other than 0)
   API_ROLE__GUEST_ = 1,	// User not belonging to any course
   API_ROLE_STUDENT = 2,	// Student in current course
   API_ROLE_TEACHER = 3,	// Teacher in current course
  } API_Role_t;

/* Translation from service-web-role to swad-core-role */
static const Rol_Role_t API_SvcRole_to_RolRole[API_NUM_ROLES] =
  {
   [API_ROLE_UNKNOWN] = Rol_UNK,
   [API_ROLE__GUEST_] = Rol_GST,
   [API_ROLE_STUDENT] = Rol_STD,
   [API_ROLE_TEACHER] = Rol_TCH,	// TODO: Create new web service role for non-editing teachers
  };

/* Translation from swad-core-role to service-web-role */
static API_Role_t API_RolRole_to_SvcRole[Rol_NUM_ROLES] =
  {
   [Rol_GST] = API_ROLE__GUEST_,
   [Rol_STD] = API_ROLE_STUDENT,
   [Rol_NET] = API_ROLE_TEACHER,	// TODO: Create new web service role for non-editing teachers
   [Rol_TCH] = API_ROLE_TEACHER,
  };

/*****************************************************************************/
/***************************** Private prototypes ****************************/
/*****************************************************************************/

static void API_Set_gSOAP_RuntimeEnv (struct soap *soap);
static void API_FreeSoapContext (struct soap *soap);

static int API_GetPlgCodFromAppKey (struct soap *soap,
                                    const char *appKey);
static int API_CheckIdSession (struct soap *soap,
			       const char *IdSes);
static int API_CheckAPIKey (char APIKey[API_BYTES_KEY + 1]);

static int API_CheckCourseAndGroupCodes (struct soap *soap,
					 long HieCod,long GrpCod);
static int API_GenerateNewAPIKey (struct soap *soap,
                                  long UsrCod,
                                  char APIKey[API_BYTES_KEY + 1]);
static Exi_Exist_t API_GetSomeUsrDataFromUsrCod (struct Usr_Data *UsrDat,
						 long HieCod);

static int API_CheckParsNewAccount (char *NewNickWithArr,		// Input
                                    char NewNickWithoutArr[Nck_MAX_BYTES_NICK_WITHOUT_ARROBA + 1],	// Output
                                    char *NewEmail,			// Input-output
                                    char *NewPlainPassword,		// Input
                                    char NewEncryptedPassword[Pwd_BYTES_ENCRYPTED_PASSWORD + 1]);	// Output

static int API_WriteSyllabusIntoHTMLBuffer (struct soap *soap,
					    Inf_Type_t InfoType,
					    char **HTMLBuffer);
static int API_WritePlainTextIntoHTMLBuffer (struct soap *soap,
					     Inf_Type_t InfoType,
                                             char **HTMLBuffer);
static int API_WritePageIntoHTMLBuffer (struct soap *soap,
				        Inf_Type_t InfoType,
                                        char **HTMLBuffer);

static void API_CopyListUsers (struct soap *soap,
                               Rol_Role_t Role,
			       struct swad__getUsersOutput *getUsersOut);
static void API_CopyUsrData (struct soap *soap,
			     struct swad__user *Usr,struct Usr_Data *UsrDat,
			     Usr_Can_t ICanSeeUsrID);

static void API_GetListGrpsInAttendanceEventFromDB (struct soap *soap,
						    long AttCod,char **ListGroups);
static void API_GetLstGrpsSel (const char *Groups);

static int API_GetMyLanguage (struct soap *soap);

static int API_SendMessageToUsr (long OriginalMsgCod,
                                 long ReplyUsrCod,long RecipientUsrCod,
                                 bool NotifyByEmail,
                                 const char *Subject,const char *Content);

static int API_GetTstTags (struct soap *soap,
			   long HieCod,struct swad__getTestsOutput *getTestsOut);
static int API_GetTstQuestions (struct soap *soap,
				long HieCod,time_t BeginTime,
				struct swad__getTestsOutput *getTestsOut);
static int API_GetTstAnswers (struct soap *soap,
		              long HieCod,time_t BeginTime,
			      struct swad__getTestsOutput *getTestsOut);
static int API_GetTstQuestionTags (struct soap *soap,
		                   long HieCod,time_t BeginTime,
				   struct swad__getTestsOutput *getTestsOut);

static void API_GetListGrpsInMatchFromDB (struct soap *soap,
					  long MchCod,char **ListGroups);

static void API_ListDir (FILE *XML,unsigned Level,const char *Path,const char *PathInTree);
static HidVis_HiddenOrVisible_t API_WriteRowFileBrowser (FILE *XML,unsigned Level,
							 Brw_FileType_t FileType,
							 const char *FileName);
static void API_IndentXMLLine (FILE *XML,unsigned Level);

static void API_GetLocationData (struct soap *soap,
                                 struct swad__location *location,
                                 time_t *CheckInTime,
                                 MYSQL_RES **mysql_res,
				 Exi_Exist_t LocationExists);
static void API_ResetLocation (struct soap *soap,
                               struct swad__location *location);

/*****************************************************************************/
/******* Function called when a web service if required by a plugin **********/
/*****************************************************************************/

static struct soap *API_soap = NULL;	// gSOAP runtime environment

static void API_Set_gSOAP_RuntimeEnv (struct soap *soap)
  {
   API_soap = soap;
  }

struct soap *API_Get_gSOAP_RuntimeEnv (void)
  {
   return API_soap;
  }

/*****************************************************************************/
/******* Function called when a web service if required by a plugin **********/
/*****************************************************************************/

void API_WebService (void)
  {
   struct soap *soap;

   if ((soap = soap_new ()))	// Allocate and initialize runtime context
     {
      soap_serve (soap);

      API_FreeSoapContext (soap);
     }
  }

/*****************************************************************************/
/******* Function called to exit on error when executing web service *********/
/*****************************************************************************/

void API_Exit (const char *DetailErrorMessage)
  {
   struct soap *soap = API_Get_gSOAP_RuntimeEnv ();
   int ReturnCode = 0;

   if (soap)
     {
      ReturnCode = (DetailErrorMessage ? soap_receiver_fault (soap,
	                                                      "Error in swad web service",
	                                                      DetailErrorMessage) :
	                                 0);

      API_FreeSoapContext (soap);
     }

   exit (ReturnCode);
  }

/*****************************************************************************/
/****************************** Free SOAP context ****************************/
/*****************************************************************************/

static void API_FreeSoapContext (struct soap *soap)
  {
   soap_destroy (soap);			// delete managed C++ objects
   soap_end (soap);			// delete managed memory
   soap_free (soap);			// free the context
   API_Set_gSOAP_RuntimeEnv (NULL);	// set pointer to NULL in order to not try to free the context again
  }

/*****************************************************************************/
/****** Check if the application key of the requester of a web service *******/
/****** is one of the application keys allowed in the plugins          *******/
/*****************************************************************************/

static int API_GetPlgCodFromAppKey (struct soap *soap,
                                    const char *appKey)
  {
   /***** Get plugin code from application key *****/
   if ((Gbl.WebService.PlgCod = Plg_DB_GetPlgCodFromAppKey (appKey)) <= 0)
      return soap_sender_fault (soap,
	                        "Unknown application key",
	                        "Unknown application");

   return SOAP_OK;
  }

/*****************************************************************************/
/****** Get the name of a web service function given the function code *******/
/*****************************************************************************/

const char *API_GetFunctionNameFromFunCod (long FunCod)
  {
   if (FunCod < 0 || FunCod > API_NUM_FUNCTIONS)
      return API_Functions[0];
   return API_Functions[FunCod];
  }

/*****************************************************************************/
/****** Check if a session identifier is valid and exists in database ********/
/*****************************************************************************/

static int API_CheckIdSession (struct soap *soap,
			       const char *IdSes)
  {
   const char *Ptr;
   unsigned i;

   /***** Check if pointer is NULL *****/
   if (IdSes == NULL)
      return soap_sender_fault (soap,
	                        "Bad session identifier",
	                        "Session identifier is a null pointer");

   /***** Check length of session identifier *****/
   if (strlen (IdSes) != Cns_BYTES_SESSION_ID)
      return soap_sender_fault (soap,
	                        "Bad session identifier",
	                        "The length of the session identifier is wrong");

   /***** Check if session identifier is in base64url *****/
   for (Ptr = IdSes;
	*Ptr;
	Ptr++)
     {
      for (i = 0;
	   i < 64;
	   i++)	// Check if this character is one of the allowed characters
         if (*Ptr == Str_BIN_TO_BASE64URL[i])
            break;
      if (i == 64)	// Character not found
         return soap_sender_fault (soap,
                                   "Bad session identifier",
                                   "The session identifier must contain only base64url characters");
     }

   /***** Query if session identifier already exists in database *****/
   if (Ses_DB_CheckIfSessionExists (IdSes) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Bad session identifier",
	                          "Session identifier does not exist in database");

   return SOAP_OK;
  }

/*****************************************************************************/
/************** Check if a web service key exists in database ****************/
/*****************************************************************************/

static int API_CheckAPIKey (char APIKey[API_BYTES_KEY + 1])
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

   /***** Set default user's code *****/
   Gbl.Usrs.Me.UsrDat.UsrCod = -1L;
   Gbl.Usrs.Me.Logged = false;
   Gbl.WebService.PlgCod = -1L;

   /***** Check that key does not exist in database *****/
   if (API_DB_GetDataFromAPIKey (&mysql_res,APIKey) == Exi_EXISTS)
     {
      row = mysql_fetch_row (mysql_res);

      Gbl.Usrs.Me.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[0]);
      Gbl.Usrs.Me.Logged = true;
      Gbl.WebService.PlgCod = Str_ConvertStrCodToLongCod (row[1]);
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/** Check if a course code and a group code are valid and exist in database **/
/*****************************************************************************/

static int API_CheckCourseAndGroupCodes (struct soap *soap,
					 long HieCod,long GrpCod)
  {
   /***** Check if course code is correct *****/
   if (HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Query if course code already exists in database *****/
   if (Crs_DB_CheckIfCrsCodExists (HieCod) == Exi_DOES_NOT_EXIST)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code does not exist in database");

   /***** Course code exists in database,
          so check if group code exists in database and belongs to course *****/
   if (GrpCod > 0)	// <= 0 means "the whole course"
      if (Grp_DB_CheckIfGrpExistsInCrs (GrpCod,HieCod) == Exi_DOES_NOT_EXIST)
         return soap_sender_fault (soap,
                                   "Bad group code",
                                   "Group code does not exist in database"
                                   " or it's not a group of the specified course");

   return SOAP_OK;
  }

/*****************************************************************************/
/***** Generate a key used in subsequents calls to other web services ********/
/*****************************************************************************/

static int API_GenerateNewAPIKey (struct soap *soap,
                                  long UsrCod,
                                  char APIKey[API_BYTES_KEY + 1])
  {
   /***** Remove expired web service keys *****/
   API_DB_RemoveOldAPIKeys ();

   /***** Create a unique name for the key *****/
   Str_Copy (APIKey,Cry_GetUniqueNameEncrypted (),API_BYTES_KEY);

   /***** Check that key does not exist in database *****/
   if (API_DB_CheckIfAPIKeyExists (APIKey) == Exi_EXISTS)
      return soap_receiver_fault (soap,
	                          "Error when generating key",
	                          "Generated key already existed in database");

   /***** Insert key into database *****/
   API_DB_CreateAPIKey (APIKey,UsrCod,Gbl.WebService.PlgCod);

   return SOAP_OK;
  }

/*****************************************************************************/
/************ Get user's data from database giving a user's code *************/
/*****************************************************************************/
// Return if UsrDat->UsrCod exists in database

static Exi_Exist_t API_GetSomeUsrDataFromUsrCod (struct Usr_Data *UsrDat,
						 long HieCod)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   Exi_Exist_t UsrExists;
   __attribute__((unused)) Err_SuccessOrError_t SuccessOrError;

   /***** Check if user's code is valid *****/
   if (UsrDat->UsrCod <= 0)
      return Exi_DOES_NOT_EXIST;

   /***** Get some user's data *****/
   if ((UsrExists = Usr_DB_GetSomeUsrDataFromUsrCod (&mysql_res,
						     UsrDat->UsrCod)) == Exi_EXISTS)
     {
      /***** Read some user's data *****/
      row = mysql_fetch_row (mysql_res);

      /* Get user's name (row[0], row[1], row[2]) and photo (row[3]) */
      Str_Copy (UsrDat->Surname1,row[0],sizeof (UsrDat->Surname1) - 1);
      Str_Copy (UsrDat->Surname2,row[1],sizeof (UsrDat->Surname2) - 1);
      Str_Copy (UsrDat->FrstName,row[2],sizeof (UsrDat->FrstName) - 1);
      Str_Copy (UsrDat->Photo   ,row[3],sizeof (UsrDat->Photo   ) - 1);

      /* Get user's brithday (row[4]) */
      SuccessOrError = Dat_GetDateFromYYYYMMDD (&(UsrDat->Birthday),row[4]);

      /***** Get list of user's IDs *****/
      ID_GetListIDsFromUsrCod (UsrDat);

      /***** Get user's nickname *****/
      Nck_DB_GetNicknameFromUsrCod (UsrDat->UsrCod,UsrDat->Nickname);

      /***** Get user's role *****/
      if (HieCod > 0)
	 /* Get the role in the given course */
	 UsrDat->Roles.InCurrentCrs = Rol_DB_GetRoleUsrInCrs (UsrDat->UsrCod,HieCod);
      else
	 /* Get the maximum role in any course */
	 UsrDat->Roles.InCurrentCrs = Rol_DB_GetMaxRoleUsrInCrss (UsrDat->UsrCod);
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return UsrExists;
  }

/*****************************************************************************/
/**************************** Get info of my marks ***************************/
/*****************************************************************************/

#define API_CHECK_NEW_ACCOUNT_OK					 0
#define API_CHECK_NEW_ACCOUNT_NICKNAME_NOT_VALID			-1
#define API_CHECK_NEW_ACCOUNT_NICKNAME_REGISTERED_BY_ANOTHER_USER	-2
#define API_CHECK_NEW_ACCOUNT_EMAIL_NOT_VALID				-3
#define API_CHECK_NEW_ACCOUNT_EMAIL_REGISTERED_BY_ANOTHER_USER		-4
#define API_CHECK_NEW_ACCOUNT_PASSWORD_NOT_VALID			-5

int swad__createAccount (struct soap *soap,
                         char *userNickname,char *userEmail,char *userPassword,char *appKey,	// input
                         struct swad__createAccountOutput *createAccountOut)			// output
  {
   char NewNickWithoutArr[Nck_MAX_BYTES_NICK_WITHOUT_ARROBA + 1];
   char NewEncryptedPassword[Pwd_BYTES_ENCRYPTED_PASSWORD + 1];
   int Result;
   int ReturnCode;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_createAccount;

   /***** Allocate space for strings *****/
   createAccountOut->wsKey = soap_malloc (soap,API_BYTES_KEY + 1);

   /***** Default values returned on error *****/
   createAccountOut->userCode = 0;	// Undefined error
   createAccountOut->wsKey[0] = '\0';

   /***** Get plugin code *****/
   if ((ReturnCode = API_GetPlgCodFromAppKey (soap,appKey)) != SOAP_OK)
      return ReturnCode;

   /***** Check parameters used to create the new account *****/
   Result = API_CheckParsNewAccount (userNickname,		// Input
                                       NewNickWithoutArr,	// Output
                                       userEmail,		// Input-output
                                       userPassword,		// Input
                                       NewEncryptedPassword);	// Output
   if (Result < 0)
     {
      createAccountOut->userCode = Result;
      return SOAP_OK;
     }

   /***** User's has no ID *****/
   Gbl.Usrs.Me.UsrDat.IDs.Num = 0;
   Gbl.Usrs.Me.UsrDat.IDs.List = NULL;

   /***** Set password to the password typed by the user *****/
   Str_Copy (Gbl.Usrs.Me.UsrDat.Password,NewEncryptedPassword,
             sizeof (Gbl.Usrs.Me.UsrDat.Password) - 1);

   /***** User does not exist in the platform, so create him/her! *****/
   Acc_CreateNewUsr (&Gbl.Usrs.Me.UsrDat,Usr_ME);

   /***** Save nickname *****/
   Nck_DB_UpdateNick (Gbl.Usrs.Me.UsrDat.UsrCod,NewNickWithoutArr);
   Str_Copy (Gbl.Usrs.Me.UsrDat.Nickname,NewNickWithoutArr,
             sizeof (Gbl.Usrs.Me.UsrDat.Nickname) - 1);

   /***** Save email *****/
   if (Mai_UpdateEmailInDB (&Gbl.Usrs.Me.UsrDat,userEmail) == Err_SUCCESS)
     {
      /* Email updated sucessfully */
      Str_Copy (Gbl.Usrs.Me.UsrDat.Email,userEmail,
                sizeof (Gbl.Usrs.Me.UsrDat.Email) - 1);
      Gbl.Usrs.Me.UsrDat.EmailConfirmed = Mai_NOT_CONFIRMED;
     }

   /***** Copy new user's code *****/
   createAccountOut->userCode = Gbl.Usrs.Me.UsrDat.UsrCod;

   /***** Generate a key used in subsequents calls to other web services *****/
   return API_GenerateNewAPIKey (soap,
				(long) createAccountOut->userCode,
				createAccountOut->wsKey);
  }

/*****************************************************************************/
/************* Get parameters for the creation of a new account **************/
/*****************************************************************************/

static int API_CheckParsNewAccount (char *NewNickWithArr,		// Input
                                    char NewNickWithoutArr[Nck_MAX_BYTES_NICK_WITHOUT_ARROBA + 1],	// Output
                                    char *NewEmail,			// Input-output
                                    char *NewPlainPassword,		// Input
                                    char NewEncryptedPassword[Pwd_BYTES_ENCRYPTED_PASSWORD + 1])	// Output
  {
   char CopyOfNewNick[Nck_MAX_BYTES_NICK_WITH_ARROBA + 1];

   /***** Step 1/3: Check new nickname *****/
   /* Make a copy without possible starting arrobas */
   switch (Nck_CheckIfNickWithArrIsValid (NewNickWithArr))
     {
      case Err_SUCCESS:	// If new nickname is valid
	 /***** Remove leading arrobas *****/
	 Str_Copy (CopyOfNewNick,NewNickWithArr,sizeof (CopyOfNewNick) - 1);
	 Str_RemoveLeadingArrobas (CopyOfNewNick);

	 /***** Check if the new nickname matches any of the nicknames of other users *****/
	 if (Nck_DB_CheckIfNickMatchesAnyNick (CopyOfNewNick))	// Already without leading arrobas
	    return API_CHECK_NEW_ACCOUNT_NICKNAME_REGISTERED_BY_ANOTHER_USER;

	 /***** Output value of nickname without leading arrobas *****/
	 Str_Copy (NewNickWithoutArr,CopyOfNewNick,Nck_MAX_BYTES_NICK_WITHOUT_ARROBA);
	 break;
      case Err_ERROR:	// New nickname is not valid
      default:
	 return API_CHECK_NEW_ACCOUNT_NICKNAME_NOT_VALID;
     }

   /***** Step 2/3: Check new email *****/
   switch (Mai_CheckIfEmailIsValid (NewEmail))
     {
      case Err_SUCCESS:	// New email is valid
	 /***** Check if the new email matches
		any of the confirmed emails of other users *****/
	 if (Mai_DB_CheckIfEmailExistsConfirmed (NewEmail) == Exi_EXISTS)
	    // An email of another user is the same that my email
	    return API_CHECK_NEW_ACCOUNT_EMAIL_REGISTERED_BY_ANOTHER_USER;
	 break;
      case Err_ERROR:	// New email is not valid
      default:
	 return API_CHECK_NEW_ACCOUNT_EMAIL_NOT_VALID;
     }

   /***** Step 3/3: Check new password *****/
   Cry_EncryptSHA512Base64 (NewPlainPassword,NewEncryptedPassword);
   if (Pwd_SlowCheckIfPasswordIsGood (NewPlainPassword,
				      NewEncryptedPassword,-1L) == Err_ERROR)        // New password is good?
      return API_CHECK_NEW_ACCOUNT_PASSWORD_NOT_VALID;

   return API_CHECK_NEW_ACCOUNT_OK;
  }

/*****************************************************************************/
/****************** Login user by user, password and key *********************/
/*****************************************************************************/

int swad__loginByUserPasswordKey (struct soap *soap,
                                  char *userID,char *userPassword,char *appKey,				// input
                                  struct swad__loginByUserPasswordKeyOutput *loginByUserPasswordKeyOut)	// output
  {
   char UsrIDNickOrEmail[Cns_MAX_BYTES_USR_LOGIN + 1];
   int ReturnCode;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   Exi_Exist_t UsrExists;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_loginByUserPasswordKey;

   /***** Allocate space for strings *****/
   loginByUserPasswordKeyOut->wsKey         = soap_malloc (soap,API_BYTES_KEY                      + 1);
   loginByUserPasswordKeyOut->userNickname  = soap_malloc (soap,Nck_MAX_BYTES_NICK_WITHOUT_ARROBA  + 1);
   loginByUserPasswordKeyOut->userID        = soap_malloc (soap,ID_MAX_BYTES_USR_ID                + 1);
   loginByUserPasswordKeyOut->userFirstname = soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginByUserPasswordKeyOut->userSurname1  = soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginByUserPasswordKeyOut->userSurname2  = soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginByUserPasswordKeyOut->userPhoto     = soap_malloc (soap,WWW_MAX_BYTES_WWW                  + 1);
   loginByUserPasswordKeyOut->userBirthday  = soap_malloc (soap,Dat_LENGTH_YYYYMMDD                + 1);

   /***** Default values returned on error *****/
   loginByUserPasswordKeyOut->userCode          = -1;
   loginByUserPasswordKeyOut->wsKey[0]          = '\0';
   loginByUserPasswordKeyOut->userNickname[0]   = '\0';
   loginByUserPasswordKeyOut->userID[0]         = '\0';
   loginByUserPasswordKeyOut->userFirstname[0]  = '\0';
   loginByUserPasswordKeyOut->userSurname1[0]   = '\0';
   loginByUserPasswordKeyOut->userSurname2[0]   = '\0';
   loginByUserPasswordKeyOut->userPhoto[0]      = '\0';
   loginByUserPasswordKeyOut->userBirthday[0]   = '\0';
   loginByUserPasswordKeyOut->userRole          = 0;	// unknown

   /***** Get plugin code *****/
   if ((ReturnCode = API_GetPlgCodFromAppKey (soap,appKey)) != SOAP_OK)
      return ReturnCode;

   /***** Check if user's email, @nickname or ID are valid *****/
   Str_Copy (UsrIDNickOrEmail,userID,sizeof (UsrIDNickOrEmail) - 1);
   switch (Nck_CheckIfNickWithArrIsValid (UsrIDNickOrEmail))
     {
      case Err_SUCCESS:		// 1: It's a nickname
	 Str_RemoveLeadingArrobas (UsrIDNickOrEmail);

	 /* User has typed a nickname */
	 Gbl.Usrs.Me.UsrDat.UsrCod = Usr_DB_GetUsrCodFromNickPwd (UsrIDNickOrEmail,
								  userPassword);
	 break;
      case Err_ERROR:
      default:
	 switch (Mai_CheckIfEmailIsValid (UsrIDNickOrEmail))
	   {
	    case Err_SUCCESS:	// 2: It's an email
	       /* User has typed an email */
	       // TODO: Get only if email confirmed?
	       Gbl.Usrs.Me.UsrDat.UsrCod = Usr_DB_GetUsrCodFromEmailPwd (UsrIDNickOrEmail,
									 userPassword);
	       break;
	    case Err_ERROR:	// 3: It's not a nickname nor email
	    default:
	       // Users' IDs are always stored internally in capitals and without leading zeros
	       Str_RemoveLeadingZeros (UsrIDNickOrEmail);
	       Str_ConvertToUpperText (UsrIDNickOrEmail);
	       switch (ID_CheckIfUsrIDIsValid (UsrIDNickOrEmail))
		 {
		  case Err_SUCCESS:
		     /* User has typed a valid user's ID (existing or not) */
		     Gbl.Usrs.Me.UsrDat.UsrCod = Usr_DB_GetUsrCodFromIDPwd (UsrIDNickOrEmail,
									    userPassword);
		     break;
		  case Err_ERROR:	// String is not a valid user's nickname, email or ID
		  default:
		     return soap_receiver_fault (soap,
						 "Bad log in",
						 "User's ID or nickname don't exist or password is wrong");
		 }
	       break;
	   }
	 break;
     }

   /***** Get user's data from database *****/
   if (Gbl.Usrs.Me.UsrDat.UsrCod > 0)	// User found in table of users' data
      /***** Get user's data *****/
      UsrExists = API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,-1L);	// Get some user's data from database
   else
      UsrExists = Exi_DOES_NOT_EXIST;

   switch (UsrExists)
     {
      case Exi_EXISTS:
	 Gbl.Usrs.Me.Logged = true;
	 Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

	 loginByUserPasswordKeyOut->userCode = (int) Gbl.Usrs.Me.UsrDat.UsrCod;

	 Str_Copy (loginByUserPasswordKeyOut->userNickname,
		   Gbl.Usrs.Me.UsrDat.Nickname,Nck_MAX_BYTES_NICK_WITHOUT_ARROBA);

	 if (Gbl.Usrs.Me.UsrDat.IDs.Num)
	    Str_Copy (loginByUserPasswordKeyOut->userID,
		      Gbl.Usrs.Me.UsrDat.IDs.List[0].ID,	// TODO: What user's ID?
		      ID_MAX_BYTES_USR_ID);

	 Str_Copy (loginByUserPasswordKeyOut->userSurname1,
		   Gbl.Usrs.Me.UsrDat.Surname1,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);
	 Str_Copy (loginByUserPasswordKeyOut->userSurname2,
		   Gbl.Usrs.Me.UsrDat.Surname2,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);
	 Str_Copy (loginByUserPasswordKeyOut->userFirstname,
		   Gbl.Usrs.Me.UsrDat.FrstName,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

	 PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,PhotoURL);
	 Str_Copy (loginByUserPasswordKeyOut->userPhoto,PhotoURL,WWW_MAX_BYTES_WWW);

	 Str_Copy (loginByUserPasswordKeyOut->userBirthday,
		   Gbl.Usrs.Me.UsrDat.Birthday.YYYYMMDD,Dat_LENGTH_YYYYMMDD);

	 loginByUserPasswordKeyOut->userRole =
	 API_RolRole_to_SvcRole[Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs];

	 /***** Generate a key used in subsequents calls to other web services *****/
	 return API_GenerateNewAPIKey (soap,
				      (long) loginByUserPasswordKeyOut->userCode,
				      loginByUserPasswordKeyOut->wsKey);
	 break;
      case Exi_DOES_NOT_EXIST:
      default:
	 loginByUserPasswordKeyOut->userCode      = -1;
	 loginByUserPasswordKeyOut->userID        = NULL;
	 loginByUserPasswordKeyOut->userSurname1  = NULL;
	 loginByUserPasswordKeyOut->userSurname2  = NULL;
	 loginByUserPasswordKeyOut->userFirstname = NULL;
	 loginByUserPasswordKeyOut->userPhoto     = NULL;
	 loginByUserPasswordKeyOut->userRole      = 0;

	 return soap_receiver_fault (soap,
				     "Bad log in",
				     "User's ID or nickname don't exist or password is wrong");
	 break;
     }
  }

/*****************************************************************************/
/************************** Login user by session ****************************/
/*****************************************************************************/

int swad__loginBySessionKey (struct soap *soap,
                             char *sessionID,char *appKey,					// input
                             struct swad__loginBySessionKeyOutput *loginBySessionKeyOut)	// output
  {
   extern Err_SuccessOrError_t (*Hie_GetDataByCod[Hie_NUM_LEVELS]) (struct Hie_Node *Node);
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   Exi_Exist_t UsrExists;
   __attribute__((unused)) Err_SuccessOrError_t SuccessOrError;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_loginBySessionKey;

   /***** Allocate space for strings *****/
   loginBySessionKeyOut->wsKey         = soap_malloc (soap,API_BYTES_KEY                   + 1);
   loginBySessionKeyOut->userNickname  = soap_malloc (soap,Nck_MAX_BYTES_NICK_WITHOUT_ARROBA  + 1);
   loginBySessionKeyOut->userID        = soap_malloc (soap,ID_MAX_BYTES_USR_ID                + 1);
   loginBySessionKeyOut->userFirstname = soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginBySessionKeyOut->userSurname1  = soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginBySessionKeyOut->userSurname2  = soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginBySessionKeyOut->userPhoto     = soap_malloc (soap,WWW_MAX_BYTES_WWW                  + 1);
   loginBySessionKeyOut->userBirthday  = soap_malloc (soap,Dat_LENGTH_YYYYMMDD                + 1);
   loginBySessionKeyOut->degreeName    = soap_malloc (soap,Nam_MAX_BYTES_FULL_NAME  + 1);
   loginBySessionKeyOut->courseName    = soap_malloc (soap,Nam_MAX_BYTES_FULL_NAME  + 1);

   /***** Default values returned on error *****/
   loginBySessionKeyOut->userCode          = -1;
   loginBySessionKeyOut->degreeCode        = -1;
   loginBySessionKeyOut->courseCode        = -1;
   loginBySessionKeyOut->wsKey[0]          = '\0';
   loginBySessionKeyOut->userNickname[0]   = '\0';
   loginBySessionKeyOut->userID[0]         = '\0';
   loginBySessionKeyOut->userFirstname[0]  = '\0';
   loginBySessionKeyOut->userSurname1[0]   = '\0';
   loginBySessionKeyOut->userSurname2[0]   = '\0';
   loginBySessionKeyOut->userPhoto[0]      = '\0';
   loginBySessionKeyOut->userBirthday[0]   = '\0';
   loginBySessionKeyOut->userRole          = Rol_UNK;
   loginBySessionKeyOut->degreeName[0]     = '\0';
   loginBySessionKeyOut->courseName[0]     = '\0';

   /***** Get plugin code *****/
   if ((ReturnCode = API_GetPlgCodFromAppKey (soap,appKey)) != SOAP_OK)
      return ReturnCode;

   /***** Check length of session identifier *****/
   if (sessionID == NULL)
      return soap_sender_fault (soap,
	                        "SessionID is null",
	                        "Login by session");

   /***** Check session identifier coming from an external plugin *****/
   if ((ReturnCode = API_CheckIdSession (soap,sessionID)) != SOAP_OK)
      return ReturnCode;

   // Now, we know that sessionID is a valid session identifier
   /***** Query data of the session from database *****/
   switch (Ses_DB_GetSomeSessionData (&mysql_res,sessionID))
     {
      case Exi_EXISTS:	// Session found in table of sessions
	 row = mysql_fetch_row (mysql_res);

	 /***** Get course (row[2]) *****/
	 Gbl.Hierarchy.Node[Hie_CRS].HieCod = Str_ConvertStrCodToLongCod (row[2]);
	 SuccessOrError = Hie_GetDataByCod[Hie_CRS] (&Gbl.Hierarchy.Node[Hie_CRS]);
	 loginBySessionKeyOut->courseCode = (int) Gbl.Hierarchy.Node[Hie_CRS].HieCod;
	 Str_Copy (loginBySessionKeyOut->courseName,Gbl.Hierarchy.Node[Hie_CRS].FullName,
		   Nam_MAX_BYTES_FULL_NAME);

	 /***** Get user code (row[0]) *****/
	 Gbl.Usrs.Me.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[0]);
	 UsrExists = API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
						   Gbl.Hierarchy.Node[Hie_CRS].HieCod);	// Get some user's data from database

	 /***** Get degree (row[1]) *****/
	 Gbl.Hierarchy.Node[Hie_DEG].HieCod = Str_ConvertStrCodToLongCod (row[1]);
	 SuccessOrError = Hie_GetDataByCod[Hie_DEG] (&Gbl.Hierarchy.Node[Hie_DEG]);
	 loginBySessionKeyOut->degreeCode = (int) Gbl.Hierarchy.Node[Hie_DEG].HieCod;
	 Str_Copy (loginBySessionKeyOut->degreeName,Gbl.Hierarchy.Node[Hie_DEG].FullName,
		   Nam_MAX_BYTES_FULL_NAME);
	 break;
      case Exi_DOES_NOT_EXIST:
      default:
	 UsrExists = Exi_DOES_NOT_EXIST;
	 break;
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   /***** Get degree of current course *****/
   Gbl.Hierarchy.Node[Hie_DEG].HieCod = Crs_DB_GetDegCodOfCourseByCod (Gbl.Hierarchy.Node[Hie_CRS].HieCod);

   switch (UsrExists)
     {
      case Exi_EXISTS:
	 Gbl.Usrs.Me.Logged = true;
	 Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

	 loginBySessionKeyOut->userCode = (int) Gbl.Usrs.Me.UsrDat.UsrCod;

	 Str_Copy (loginBySessionKeyOut->userNickname,Gbl.Usrs.Me.UsrDat.Nickname,
		   Nck_MAX_BYTES_NICK_WITHOUT_ARROBA);

	 if (Gbl.Usrs.Me.UsrDat.IDs.Num)
	    Str_Copy (loginBySessionKeyOut->userID,
		      Gbl.Usrs.Me.UsrDat.IDs.List[0].ID,	// TODO: What user's ID?
		      ID_MAX_BYTES_USR_ID);

	 Str_Copy (loginBySessionKeyOut->userSurname1,
		   Gbl.Usrs.Me.UsrDat.Surname1,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);
	 Str_Copy (loginBySessionKeyOut->userSurname2,
		   Gbl.Usrs.Me.UsrDat.Surname2,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);
	 Str_Copy (loginBySessionKeyOut->userFirstname,
		   Gbl.Usrs.Me.UsrDat.FrstName,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

	 PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,PhotoURL);
	 Str_Copy (loginBySessionKeyOut->userPhoto,PhotoURL,WWW_MAX_BYTES_WWW);

	 Str_Copy (loginBySessionKeyOut->userBirthday,
		   Gbl.Usrs.Me.UsrDat.Birthday.YYYYMMDD,Dat_LENGTH_YYYYMMDD);

	 loginBySessionKeyOut->userRole = API_RolRole_to_SvcRole[Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs];

	 /***** Generate a key used in subsequents calls to other web services *****/
	 return API_GenerateNewAPIKey (soap,
				      (long) loginBySessionKeyOut->userCode,
				      loginBySessionKeyOut->wsKey);
	 break;
      case Exi_DOES_NOT_EXIST:
      default:
	 return soap_receiver_fault (soap,
				     "Bad session identifier",
				     "Session identifier does not exist in database");
	 break;
     }
  }

/*****************************************************************************/
/***************** Get available roles in the current course *****************/
/*****************************************************************************/
/*
With this function, SWADroid is able to check if user can see the button to show the MAC address of the nearest WiFi access point
*/

int swad__getAvailableRoles (struct soap *soap,
                             char *wsKey,int courseCode,					// input
                             struct swad__getAvailableRolesOutput *getAvailableRolesOut)	// output
  {
   int ReturnCode;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getAvailableRoles;

   /***** Initialize hierarchy *****/
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;
   Hie_InitHierarchy ();

   /***** Default value returned on error *****/
   getAvailableRolesOut->roles = 0;	// error

   /***** Check API (web service) key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     -1L) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Return available roles *****/
   Rol_SetMyRoles ();
   getAvailableRolesOut->roles = Gbl.Usrs.Me.Role.Available;

   return SOAP_OK;
  }

/*****************************************************************************/
/*********************** Send a new password by email ************************/
/*****************************************************************************/

int swad__getNewPassword (struct soap *soap,
                          char *userID,char *appKey,				// input
                          struct swad__getNewPasswordOutput *getNewPasswordOut)	// output
  {
   int ReturnCode;
   char UsrIDNickOrEmail[Cns_MAX_BYTES_USR_LOGIN + 1];
   char NewRandomPlainPassword[Pwd_MAX_BYTES_PLAIN_PASSWORD + 1];

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getNewPassword;

   /***** Default value returned on error *****/
   getNewPasswordOut->success = 0;	// error

   /***** Get plugin code *****/
   if ((ReturnCode = API_GetPlgCodFromAppKey (soap,appKey)) != SOAP_OK)
      return ReturnCode;

   /***** Check if user's email, @nickname or ID are valid *****/
   Str_Copy (UsrIDNickOrEmail,userID,sizeof (UsrIDNickOrEmail) - 1);
   switch (Nck_CheckIfNickWithArrIsValid (UsrIDNickOrEmail))
     {
      case Err_SUCCESS:		// 1: It's a nickname
	 Str_RemoveLeadingArrobas (UsrIDNickOrEmail);

	 /* User has typed a nickname */
	 Gbl.Usrs.Me.UsrDat.UsrCod = Usr_DB_GetUsrCodFromNick (UsrIDNickOrEmail);
	 break;
      case Err_ERROR:
      default:
	 switch (Mai_CheckIfEmailIsValid (Gbl.Usrs.Me.UsrIdLogin))
	   {
	    case Err_SUCCESS:	// 2: It's an email
	    /* User has typed an email */
	    Gbl.Usrs.Me.UsrDat.UsrCod = Usr_DB_GetUsrCodFromEmail (UsrIDNickOrEmail);
	       break;
	    case Err_ERROR:	// 3: It's not a nickname nor email
	    default:
	       // Users' IDs are always stored internally in capitals and without leading zeros
	       Str_RemoveLeadingZeros (UsrIDNickOrEmail);
	       Str_ConvertToUpperText (UsrIDNickOrEmail);
	       switch (ID_CheckIfUsrIDIsValid (UsrIDNickOrEmail))
		 {
		  case Err_SUCCESS:
		     /* User has typed a valid user's ID (existing or not) */
		     Gbl.Usrs.Me.UsrDat.UsrCod = Usr_DB_GetUsrCodFromID (UsrIDNickOrEmail);
		     break;
		  case Err_ERROR:	// String is not a valid user's nickname, email or ID
		  default:
		     return soap_receiver_fault (soap,
						 "Bad log in",
						 "User's email, nickname or ID don't exist");
		 }
	       break;
	   }
	 break;
     }

   /***** Get user's data from database *****/
   if (Gbl.Usrs.Me.UsrDat.UsrCod > 0)	// One unique user found in table of users' data
     {
      Usr_GetUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,	// Get my data
                                Usr_DONT_GET_PREFS,
                                Usr_DONT_GET_ROLE_IN_CRS);

      if (Gbl.Usrs.Me.UsrDat.Email[0])
	 if (Pwd_SendNewPasswordByEmail (NewRandomPlainPassword) == 0) // Message sent successfully
	   {
	    Pwd_SetMyPendingPassword (NewRandomPlainPassword);
	    getNewPasswordOut->success = 1;
	   }
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/************************ Return courses of a user ***************************/
/*****************************************************************************/

int swad__getCourses (struct soap *soap,
                      char *wsKey,					// input
                      struct swad__getCoursesOutput *getCoursesOut)	// output
  {
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumCrs;
   unsigned NumCrss;
   Rol_Role_t Role;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getCourses;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     -1L) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Query my courses from database *****/
   NumCrss = Enr_DB_GetMyCoursesNames (&mysql_res);

   getCoursesOut->numCourses          =
   getCoursesOut->coursesArray.__size = (int) NumCrss;

   if (NumCrss == 0)
      getCoursesOut->coursesArray.__ptr = NULL;
   else	// Courses found
     {
      getCoursesOut->coursesArray.__ptr = soap_malloc (soap,
						       (getCoursesOut->coursesArray.__size) *
						       sizeof (*(getCoursesOut->coursesArray.__ptr)));

      for (NumCrs = 0;
	   NumCrs < NumCrss;
	   NumCrs++)
	{
	 /* Get next course */
	 row = mysql_fetch_row (mysql_res);

         /* Get course code (row[0]) */
	 getCoursesOut->coursesArray.__ptr[NumCrs].courseCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get course short name (row[1])
            and course full name (row[2]) */
         getCoursesOut->coursesArray.__ptr[NumCrs].courseShortName =
            soap_malloc (soap,Nam_MAX_BYTES_SHRT_NAME + 1);
         getCoursesOut->coursesArray.__ptr[NumCrs].courseFullName =
            soap_malloc (soap,Nam_MAX_BYTES_FULL_NAME + 1);
	 Str_Copy (getCoursesOut->coursesArray.__ptr[NumCrs].courseShortName,
	           row[1],Nam_MAX_BYTES_SHRT_NAME);
	 Str_Copy (getCoursesOut->coursesArray.__ptr[NumCrs].courseFullName,
	           row[2],Nam_MAX_BYTES_FULL_NAME);

         /* Get role (row[3]) */
         if (sscanf (row[3],"%u",&Role) != 1)	// Role in this course
            Role = Rol_UNK;
         getCoursesOut->coursesArray.__ptr[NumCrs].userRole = API_RolRole_to_SvcRole[Role];
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************ Return course information **************************/
/*****************************************************************************/
// TODO: Not completely implemented

int swad__getCourseInfo (struct soap *soap,
                         char *wsKey,int courseCode,char *infoType,		// input
                         struct swad__getCourseInfoOutput *getCourseInfo)	// output
  {
   int ReturnCode;
   struct Inf_Info Info;
   Inf_Type_t InfoType;
   size_t Length;
   int Result = SOAP_OK;
   static const char *NamesInWSForInfoType[Inf_NUM_TYPES] =
     {
      [Inf_UNKNOWN_TYPE	] = NULL,
      [Inf_INFORMATION	] = "introduction",
      [Inf_PROGRAM	] = "program",	// Not used
      [Inf_TEACH_GUIDE	] = "guide",
      [Inf_SYLLABUS_LEC	] = "lectures",
      [Inf_SYLLABUS_PRA	] = "practicals",
      [Inf_BIBLIOGRAPHY	] = "bibliography",
      [Inf_FAQ		] = "FAQ",
      [Inf_LINKS	] = "links",
      [Inf_ASSESSMENT	] = "assessment",
     };
   static const char *NamesInWSForInfoSrc[Inf_NUM_SOURCES] =
     {
      [Inf_SRC_NONE	] = "none",
      [Inf_EDITOR	] = "editor",
      [Inf_PLAIN_TEXT	] = "plainText",
      [Inf_RICH_TEXT	] = "richText",
      [Inf_PAGE		] = "page",
      [Inf_URL		] = "URL",
     };

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getCourseInfo;

   /***** Initialize hierarchy *****/
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;
   Hie_InitHierarchy ();

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (soap,
						   Gbl.Hierarchy.Node[Hie_CRS].HieCod,
						   -1L)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get info source *****/
   for (InfoType  = (Inf_Type_t) 1;
	InfoType <= (Inf_Type_t) (Inf_NUM_TYPES - 1);
	InfoType++)
      if (!strcmp (infoType,NamesInWSForInfoType[InfoType]))
	 break;
   if (InfoType == Inf_NUM_TYPES)	// Not found!
      return soap_receiver_fault (soap,
	                          "Bad info type",
	                          "Unknown requested info type");
   Info.Type = InfoType;
   Inf_GetAndCheckInfoSrcFromDB (&Info);

   Length = strlen (NamesInWSForInfoSrc[Info.FromDB.Src]);
   getCourseInfo->infoSrc = soap_malloc (soap,Length + 1);
   Str_Copy (getCourseInfo->infoSrc,NamesInWSForInfoSrc[Info.FromDB.Src],Length);

   /***** Get info text *****/
   getCourseInfo->infoTxt = NULL;
   switch (Info.FromDB.Src)
     {
      case Inf_SRC_NONE:	// No info available
         break;
      case Inf_EDITOR:		// Internal editor (only for syllabus)
	 switch (InfoType)
	   {
	    case Inf_SYLLABUS_LEC:	// Syllabus (lectures)
	    case Inf_SYLLABUS_PRA:	// Syllabys (practicals)
	       Result = API_WriteSyllabusIntoHTMLBuffer (soap,Info.Type,&(getCourseInfo->infoTxt));
	       break;
	    default:
               break;
	   }
	 break;
      case Inf_PLAIN_TEXT:	// Plain text
      case Inf_RICH_TEXT:	// Rich text (not yet available)
	 Result = API_WritePlainTextIntoHTMLBuffer (soap,Info.Type,&(getCourseInfo->infoTxt));
         break;
      case Inf_PAGE:		// Web page hosted in SWAD server
	 Result = API_WritePageIntoHTMLBuffer (soap,Info.Type,&(getCourseInfo->infoTxt));
         break;
      case Inf_URL:		// Link to a web page
         getCourseInfo->infoTxt = soap_malloc (soap,WWW_MAX_BYTES_WWW + 1);
         Inf_WriteURLIntoTxtBuffer (InfoType,getCourseInfo->infoTxt);
         break;
     }

   /***** Return empty text if pointer is null *****/
   if (getCourseInfo->infoTxt == NULL)
     {
      getCourseInfo->infoTxt = soap_malloc (soap,1);
      getCourseInfo->infoTxt[0] = '\0';
     }

   return Result;
  }

/*****************************************************************************/
/************** Write the syllabus into a temporary HTML file ****************/
/*****************************************************************************/

static int API_WriteSyllabusIntoHTMLBuffer (struct soap *soap,
					    Inf_Type_t InfoType,
					    char **HTMLBuffer)
  {
   char FileNameHTMLTmp[PATH_MAX + 1];
   FILE *FileHTMLTmp;
   size_t Length;

   /***** Initialize buffer *****/
   *HTMLBuffer = NULL;

   /***** Get list of tree nodes *****/
   Tre_GetListNodes (InfoType);
   if (Tre_GetNumNodes ())
     {
      /***** Create a unique name for the file *****/
      snprintf (FileNameHTMLTmp,sizeof (FileNameHTMLTmp),"%s/%s_tree.html",
	        Cfg_PATH_OUT_PRIVATE,Cry_GetUniqueNameEncrypted ());

      /***** Create a new temporary file for writing and reading *****/
      if ((FileHTMLTmp = fopen (FileNameHTMLTmp,"w+b")) == NULL)
	{
	 Tre_FreeListNodes ();
         return soap_receiver_fault (soap,
                                     "Syllabus can not be copied into buffer",
                                     "Can not create temporary file");
	}

      /***** Write syllabus in HTML into a temporary file *****/
      Tre_WriteTreeIntoHTMLTmpFile (InfoType,FileHTMLTmp);

      /***** Write syllabus from list of items in memory to text buffer *****/
      /* Compute length of file */
      Length = (size_t) ftell (FileHTMLTmp);

      /* Allocate memory for buffer */
      if ((*HTMLBuffer = malloc (Length + 1)) == NULL)
	{
	 fclose (FileHTMLTmp);
	 unlink (FileNameHTMLTmp);
	 Tre_FreeListNodes ();
         return soap_receiver_fault (soap,
                                     "Syllabus can not be copied into buffer",
                                     "Not enough memory for buffer");
	}

      /* Copy file content into buffer */
      fseek (FileHTMLTmp,0L,SEEK_SET);
      if (fread (*HTMLBuffer,sizeof (char),Length,FileHTMLTmp) != Length)
	{
	 fclose (FileHTMLTmp);
	 unlink (FileNameHTMLTmp);
	 Tre_FreeListNodes ();
         return soap_receiver_fault (soap,
                                     "Syllabus can not be copied into buffer",
                                     "Error reading file into buffer");
	}
      (*HTMLBuffer)[Length] = '\0';

      /***** Close and remove temporary file *****/
      fclose (FileHTMLTmp);
      unlink (FileNameHTMLTmp);
     }

   /***** Free list of tree nodes *****/
   Tre_FreeListNodes ();

   return SOAP_OK;
  }

/*****************************************************************************/
/************* Check if exists and write page into HTML buffer ***************/
/*****************************************************************************/

static int API_WritePlainTextIntoHTMLBuffer (struct soap *soap,
					     Inf_Type_t InfoType,
                                             char **HTMLBuffer)
  {
   extern const char *Txt_INFO_TITLE[Inf_NUM_TYPES];
   char TxtHTML[Cns_MAX_BYTES_LONG_TEXT + 1];
   char FileNameHTMLTmp[PATH_MAX + 1];
   FILE *FileHTMLTmp;
   size_t Length;

   /***** Initialize buffer *****/
   *HTMLBuffer = NULL;

   /***** Get info text from database *****/
   Inf_GetInfoTxtFromDB (Gbl.Hierarchy.Node[Hie_CRS].HieCod,InfoType,
                         TxtHTML,NULL);

   if (TxtHTML[0])
     {
      /***** Create a unique name for the file *****/
      snprintf (FileNameHTMLTmp,sizeof (FileNameHTMLTmp),"%s/%s_info.html",
	        Cfg_PATH_OUT_PRIVATE,Cry_GetUniqueNameEncrypted ());

      /***** Create a new temporary file for writing and reading *****/
      if ((FileHTMLTmp = fopen (FileNameHTMLTmp,"w+b")) == NULL)
         return soap_receiver_fault (soap,
                                     "Plain text can not be copied into buffer",
                                     "Can not create temporary file");

      /***** Write start of HTML code *****/
      Lay_BeginHTMLFile (FileHTMLTmp,Txt_INFO_TITLE[InfoType]);
      fprintf (FileHTMLTmp,"<body>\n"
                           "<div class=\"LM DAT_%s\">\n",The_GetSuffix ());

      /***** Write plain text into text buffer *****/
      /* Convert to respectful HTML and insert links */
      Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML,
                        TxtHTML,Cns_MAX_BYTES_LONG_TEXT,Str_DONT_REMOVE_SPACES);
      ALn_InsertLinks (TxtHTML,Cns_MAX_BYTES_LONG_TEXT,60);	// Insert links

      /* Write text */
      fprintf (FileHTMLTmp,"%s",TxtHTML);

      /***** Write end of page into file *****/
      fprintf (FileHTMLTmp,"</div>\n"
			   "</html>\n"
			   "</body>\n");

      /***** Compute length of file *****/
      Length = (size_t) ftell (FileHTMLTmp);

      /***** Allocate memory for buffer *****/
      if ((*HTMLBuffer = malloc (Length + 1)) == NULL)
	{
	 fclose (FileHTMLTmp);
	 unlink (FileNameHTMLTmp);
         return soap_receiver_fault (soap,
                                     "Plain text can not be copied into buffer",
                                     "Not enough memory for buffer");
	}

      /***** Copy file content into buffer *****/
      fseek (FileHTMLTmp,0L,SEEK_SET);
      if (fread (*HTMLBuffer,sizeof (char),Length,FileHTMLTmp) != Length)
	{
	 fclose (FileHTMLTmp);
	 unlink (FileNameHTMLTmp);
         return soap_receiver_fault (soap,
                                     "Plain text can not be copied into buffer",
                                     "Error reading file into buffer");
	}
      (*HTMLBuffer)[Length] = '\0';

      /***** Close and remove temporary file *****/
      fclose (FileHTMLTmp);
      unlink (FileNameHTMLTmp);
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/************* Check if exists and write page into HTML buffer ***************/
/*****************************************************************************/

static int API_WritePageIntoHTMLBuffer (struct soap *soap,
				        Inf_Type_t InfoType,
                                        char **HTMLBuffer)
  {
   char PathRelDirHTML[PATH_MAX + 1];
   char PathRelFileHTML[PATH_MAX + 1 + 10 + 1];
   FILE *FileHTML;
   Exi_Exist_t FileExists;
   size_t Length;

   /***** Initialize buffer *****/
   *HTMLBuffer = NULL;

   /***** Build path of directory containing web page *****/
   Inf_BuildPathPage (Gbl.Hierarchy.Node[Hie_CRS].HieCod,InfoType,PathRelDirHTML);

   /***** Open file with web page *****/
   /* 1. Check if index.html exists */
   snprintf (PathRelFileHTML,sizeof (PathRelFileHTML),"%s/index.html",
	     PathRelDirHTML);
   FileExists = Fil_CheckIfPathExists (PathRelFileHTML);	// TODO: Check if not empty?
   if (FileExists == Exi_DOES_NOT_EXIST)
     {
      /* 2. If index.html not exists, try index.htm */
      snprintf (PathRelFileHTML,sizeof (PathRelFileHTML),"%s/index.htm",
		PathRelDirHTML);
      FileExists = Fil_CheckIfPathExists (PathRelFileHTML);	// TODO: Check if not empty?
     }

   if (FileExists == Exi_EXISTS)
     {
      /***** Write page from file to text buffer *****/
      /* Open file */
      if ((FileHTML = fopen (PathRelFileHTML,"rb")) == NULL)
	 Err_FileFolderNotFoundExit ();

      /* Compute file size */
      fseek (FileHTML,0L,SEEK_END);
      Length = (size_t) ftell (FileHTML);
      fseek (FileHTML,0L,SEEK_SET);

      /* Allocate memory for buffer */
      if ((*HTMLBuffer = malloc (Length + 1)) == NULL)
	{
	 fclose (FileHTML);
	 Err_NotEnoughMemoryExit ();
         return soap_receiver_fault (soap,
                                     "Web page can not be copied into buffer",
                                     "Not enough memory for buffer");
	}

      /* Copy file content into buffer */
      if (fread (*HTMLBuffer,sizeof (char),Length,FileHTML) != Length)
	{
	 fclose (FileHTML);
         return soap_receiver_fault (soap,
                                     "Web page can not be copied into buffer",
                                     "Error reading web page into buffer");
	}
      (*HTMLBuffer)[Length] = '\0';

      /***** Close HTML file *****/
      fclose (FileHTML);
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/************* Get users in a course (and optionally in groups) **************/
/*****************************************************************************/

int swad__getUsers (struct soap *soap,
                    char *wsKey,int courseCode,char *groups,int userRole,	// input
                    struct swad__getUsersOutput *getUsersOut)			// output
  {
   int ReturnCode;
   Rol_Role_t Role;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getUsers;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = courseCode > 0 ? (long) courseCode :
							 -1L;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check course *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (soap,
						   Gbl.Hierarchy.Node[Hie_CRS].HieCod,
						   -1L)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
				  "Request forbidden",
				  "Requester must belong to course");

   /***** Get degree of current course *****/
   Gbl.Hierarchy.Node[Hie_DEG].HieCod = Crs_DB_GetDegCodOfCourseByCod (Gbl.Hierarchy.Node[Hie_CRS].HieCod);

   /***** Check requested users' role *****/
   if (userRole != API_ROLE_STUDENT &&	// Students
       userRole != API_ROLE_TEACHER)	// Teachers
      return soap_sender_fault (soap,
	                        "Bad requested users' type",
	                        "User roles allowed are 2 (students) or 3 (teachers)");
   Role = API_SvcRole_to_RolRole[userRole];

   /***** Create a list of groups selected *****/
   API_GetLstGrpsSel (groups);
   if (Gbl.Crs.Grps.LstGrpsSel.NumGrps)
      /***** Get list of groups types and groups in current course *****/
      Grp_GetListGrpTypesInCurrentCrs (Grp_GRP_TYPES_WITH_GROUPS);

   /***** Get list of users *****/
   Usr_GetListUsrs (Hie_CRS,Role);
   API_CopyListUsers (soap,
		      Role,getUsersOut);
   Usr_FreeUsrsList (Role);

   if (Gbl.Crs.Grps.LstGrpsSel.NumGrps)
     {
      /***** Free list of groups types and groups in current course *****/
      Grp_FreeListGrpTypesAndGrps ();

      /***** Free memory for list of selected groups *****/
      Grp_FreeListCodSelectedGrps ();
     }

   return SOAP_OK;
  }

int swad__findUsers (struct soap *soap,
                     char *wsKey,int courseCode,char *filter,int userRole,	// input
                     struct swad__getUsersOutput *getUsersOut)			// output
  {
   int ReturnCode;
   struct Sch_Search Search;
   char SearchQuery[Sch_MAX_BYTES_SEARCH_QUERY + 1];
   Rol_Role_t Role;
   Hie_Level_t HieLvl;
   bool FilterTooShort = false;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_findUsers;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = courseCode > 0 ? (long) courseCode :
							 -1L;
   Hie_InitHierarchy ();

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   if (Gbl.Hierarchy.HieLvl == Hie_CRS)	// Course selected
      /***** Check course *****/
      if ((ReturnCode = API_CheckCourseAndGroupCodes (soap,
						      Gbl.Hierarchy.Node[Hie_CRS].HieCod,
						      -1L)) != SOAP_OK)
	 return ReturnCode;

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   if (Gbl.Hierarchy.HieLvl == Hie_CRS)	// Course selected
      /***** Check if I am a student, non-editing teacher or teacher in the course *****/
      if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
          Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
	  Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
	 return soap_receiver_fault (soap,
				     "Request forbidden",
				     "Requester must belong to course");

   if (Gbl.Hierarchy.HieLvl == Hie_CRS)
      /***** Get degree of current course *****/
      Gbl.Hierarchy.Node[Hie_DEG].HieCod = Crs_DB_GetDegCodOfCourseByCod (Gbl.Hierarchy.Node[Hie_CRS].HieCod);

   /***** Check requested users' role *****/
   if (userRole < API_ROLE_UNKNOWN ||
       userRole > API_ROLE_TEACHER)
      return soap_sender_fault (soap,
	                        "Bad requested users' type",
	                        "User roles allowed are 0 (all), 1 (guests), 2 (students) or 3 (teachers)");
   Role = API_SvcRole_to_RolRole[userRole];

   /***** Query users beloging to course or group from database *****/
   Search.WhatToSearch = Sch_SEARCH_USERS;
   Str_Copy (Search.Str,filter,sizeof (Search.Str) - 1);

   if (Search.Str[0])	// Search some users
     {
      HieLvl = Gbl.Hierarchy.HieLvl == Hie_CRS ? Hie_CRS :
						 Hie_SYS;
      switch (Sch_BuildSearchQuery (SearchQuery,&Search,
				    "CONCAT_WS(' ',FirstName,Surname1,Surname2)",
				    NULL,NULL))
	{
	 case Err_SUCCESS:
	    /***** Create temporary table with candidate users *****/
	    // Search is faster (aproximately x2) using temporary tables
	    Usr_DB_CreateTmpTableAndSearchCandidateUsrs (SearchQuery);

	    /***** Search for users *****/
	    Usr_SearchListUsrs (HieLvl,Role);
	    API_CopyListUsers (soap,
			       Role,getUsersOut);
	    Usr_FreeUsrsList (Role);

	    /***** Drop temporary table with candidate users *****/
	    Usr_DB_DropTmpTableWithCandidateUsrs ();
	    break;
	 case Err_ERROR:
	 default:
	    FilterTooShort = true;
	    break;
	}
     }
   else
      FilterTooShort = true;

   /***** Return error in filter? *****/
   if (FilterTooShort)
     {
      getUsersOut->numUsers = -1;	// < 0 ==> filter too short
      getUsersOut->usersArray.__size = 0;
      getUsersOut->usersArray.__ptr = NULL;
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/***************************** Copy users from list **************************/
/*****************************************************************************/

static void API_CopyListUsers (struct soap *soap,
                               Rol_Role_t Role,
			       struct swad__getUsersOutput *getUsersOut)
  {
   unsigned NumUsrs;
   unsigned NumUsr;
   struct Usr_Data UsrDat;
   Usr_Can_t ICanSeeUsrID;

   /***** Initialize result *****/
   getUsersOut->numUsers = 0;
   getUsersOut->usersArray.__size = 0;
   getUsersOut->usersArray.__ptr = NULL;

   NumUsrs = Gbl.Usrs.LstUsrs[Role].NumUsrs;
   if (NumUsrs)
     {
      getUsersOut->numUsers = (int) NumUsrs;
      getUsersOut->usersArray.__size = (int) NumUsrs;
      getUsersOut->usersArray.__ptr = soap_malloc (soap,
						   (getUsersOut->usersArray.__size) *
						   sizeof (*(getUsersOut->usersArray.__ptr)));

      /***** Initialize structure with user's data *****/
      Usr_UsrDataConstructor (&UsrDat);

      /***** List data of users *****/
      for (NumUsr = 0;
	   NumUsr < NumUsrs;
	   NumUsr++)
	{
	 /* Copy user's basic data from list */
         Usr_CopyBasicUsrDataFromList (&UsrDat,&Gbl.Usrs.LstUsrs[Role].Lst[NumUsr]);

	 /* Get list of user's IDs */
         ID_GetListIDsFromUsrCod (&UsrDat);
         ICanSeeUsrID = ID_ICanSeeOtherUsrIDs (&UsrDat);

	 /* Get nickname */
         Nck_DB_GetNicknameFromUsrCod (UsrDat.UsrCod,UsrDat.Nickname);

	 /* Copy user's data into output structure */
	 API_CopyUsrData (soap,
			  &(getUsersOut->usersArray.__ptr[NumUsr]),
			  &UsrDat,
			  ICanSeeUsrID);
	}

      /***** Free memory used for user's data *****/
      Usr_UsrDataDestructor (&UsrDat);
     }
  }

/*****************************************************************************/
/********************** Return group types in a course ***********************/
/*****************************************************************************/

int swad__getGroupTypes (struct soap *soap,
                         char *wsKey,int courseCode,				// input
                         struct swad__getGroupTypesOutput *getGroupTypesOut)	// output
  {
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumGrpTyp;
   unsigned NumGrpTypes;
   long OpenTime;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getGroupTypes;

   /***** Initialize hierarchy *****/
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;
   Hie_InitHierarchy ();

   /***** Open groups of this course that must be opened
          if open time is in the past *****/
   Grp_OpenGroupsAutomatically ();

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Query group types in a course from database *****/
   NumGrpTypes = Grp_DB_GetAllGrpTypesInCrs (&mysql_res,courseCode);

   getGroupTypesOut->numGroupTypes = (int) NumGrpTypes;
   getGroupTypesOut->groupTypesArray.__size = (int) NumGrpTypes;

   if (NumGrpTypes == 0)
      getGroupTypesOut->groupTypesArray.__ptr = NULL;
   else	// Groups found
     {
      getGroupTypesOut->groupTypesArray.__ptr = soap_malloc (soap,
							     (getGroupTypesOut->groupTypesArray.__size) *
							     sizeof (*(getGroupTypesOut->groupTypesArray.__ptr)));

      for (NumGrpTyp = 0;
	   NumGrpTyp < NumGrpTypes;
	   NumGrpTyp++)
	{
	 /* Get next group */
	 row = mysql_fetch_row (mysql_res);

	 /* Get group type code (row[0]) */
	 getGroupTypesOut->groupTypesArray.__ptr[NumGrpTyp].groupTypeCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get group type name (row[1]) */
         getGroupTypesOut->groupTypesArray.__ptr[NumGrpTyp].groupTypeName =
            soap_malloc (soap,Grp_MAX_BYTES_GROUP_TYPE_NAME + 1);
	 Str_Copy (getGroupTypesOut->groupTypesArray.__ptr[NumGrpTyp].groupTypeName,
	           row[1],Grp_MAX_BYTES_GROUP_TYPE_NAME);

         /* Get whether enrolment is mandatory ('Y') or voluntary ('N') (row[2]) */
         getGroupTypesOut->groupTypesArray.__ptr[NumGrpTyp].mandatory = row[2][0] == 'Y' ? 1 :
                                                                                           0;

         /* Get whether user can enrol in multiple groups ('Y') or only in one group ('N') (row[3]) */
         getGroupTypesOut->groupTypesArray.__ptr[NumGrpTyp].multiple = row[3][0] == 'Y' ? 1 :
                                                                                          0;

         // Whether groups of this type must be opened (row[4]) ignored here

         /* Get time of opening (row[5]) */
         OpenTime = 0L;
         if (row[5])
            sscanf (row[5],"%ld",&OpenTime);
         getGroupTypesOut->groupTypesArray.__ptr[NumGrpTyp].openTime = OpenTime;

         // Number of groups of this type (row[6]) ignored here
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************** Return groups in a course ************************/
/*****************************************************************************/

int swad__getGroups (struct soap *soap,
                     char *wsKey,int courseCode,			// input
                     struct swad__getGroupsOutput *getGroupsOut)	// output
  {
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumGrps;
   unsigned NumGrp;
   long GrpCod;
   unsigned MaxStudents;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getGroups;

   /***** Initialize hierarchy *****/
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;
   Hie_InitHierarchy ();

   /***** Open groups of this course that must be opened
          if open time is in the past *****/
   Grp_OpenGroupsAutomatically ();

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Query groups in a course from database *****/
   NumGrps = Grp_DB_GetGrpsInCrs (&mysql_res,courseCode);

   getGroupsOut->numGroups = (int) NumGrps;
   getGroupsOut->groupsArray.__size = (int) NumGrps;

   if (NumGrps == 0)
      getGroupsOut->groupsArray.__ptr = NULL;
   else	// Groups found
     {
      getGroupsOut->groupsArray.__ptr = soap_malloc (soap,
						     (getGroupsOut->groupsArray.__size) *
						     sizeof (*(getGroupsOut->groupsArray.__ptr)));

      for (NumGrp = 0;
	   NumGrp < NumGrps;
	   NumGrp++)
	{
	 /* Get next group */
	 row = mysql_fetch_row (mysql_res);

	 /* Get group type code (row[0]) */
	 getGroupsOut->groupsArray.__ptr[NumGrp].groupTypeCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get group type name (row[1]) */
         getGroupsOut->groupsArray.__ptr[NumGrp].groupTypeName =
            soap_malloc (soap,Grp_MAX_BYTES_GROUP_TYPE_NAME + 1);
	 Str_Copy (getGroupsOut->groupsArray.__ptr[NumGrp].groupTypeName,row[1],
	           Grp_MAX_BYTES_GROUP_TYPE_NAME);

         /* Get group code (row[2]) */
	 GrpCod = Str_ConvertStrCodToLongCod (row[2]);
	 getGroupsOut->groupsArray.__ptr[NumGrp].groupCode = (int) GrpCod;

         /* Get group name (row[3]) */
         getGroupsOut->groupsArray.__ptr[NumGrp].groupName =
            soap_malloc (soap,Grp_MAX_BYTES_GROUP_NAME + 1);
	 Str_Copy (getGroupsOut->groupsArray.__ptr[NumGrp].groupName,row[3],
	           Grp_MAX_BYTES_GROUP_NAME);

         /* Get max number of students of group (row[4]) and number of current students */
         MaxStudents = Grp_ConvertToNumMaxStdsGrp (row[4]);
         getGroupsOut->groupsArray.__ptr[NumGrp].maxStudents = MaxStudents > Grp_MAX_STUDENTS_IN_A_GROUP ? -1 :
                                                                                                           (int) MaxStudents;

         /* Get number of current students */
         getGroupsOut->groupsArray.__ptr[NumGrp].numStudents = (int) Grp_DB_CountNumUsrsInGrp (Rol_STD,GrpCod);

         /* Get whether group is open ('Y') or closed ('N') (row[5]) */
         getGroupsOut->groupsArray.__ptr[NumGrp].open = row[5][0] == 'Y' ? 1 :
                                                                           0;

         /* Get whether group have file zones ('Y') or not ('N') (row[6]) */
         getGroupsOut->groupsArray.__ptr[NumGrp].fileZones = row[6][0] == 'Y' ? 1 :
                                                                                0;

         /* Get whether I belong to this group or not */
         getGroupsOut->groupsArray.__ptr[NumGrp].member = Grp_GetIfIBelongToGrp (GrpCod) == Usr_BELONG ? 1 :
													 0;
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************* Send my groups in a course ************************/
/*****************************************************************************/

int swad__sendMyGroups (struct soap *soap,
                        char *wsKey,int courseCode,char *myGroups,		// input
                        struct swad__sendMyGroupsOutput *SendMyGroupsOut)	// output
  {
   int ReturnCode;
   struct ListCodGrps LstGrpsIWant;
   const char *Ptr;
   char LongStr[Cns_MAX_DIGITS_LONG + 1];
   unsigned NumGrps;
   unsigned NumGrp;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   long GrpCod;
   unsigned MaxStudents;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_sendMyGroups;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get the group codes which I want to join to *****/
   LstGrpsIWant.NumGrps = 0;
   LstGrpsIWant.GrpCods = NULL;

   if (myGroups[0])
     {
      /***** Count number of desired groups *****/
      for (NumGrp = 0, Ptr = myGroups;
	   *Ptr;
	   NumGrp++)
	 Str_GetNextStringUntilComma (&Ptr,LongStr,Cns_MAX_DIGITS_LONG);
      LstGrpsIWant.NumGrps = NumGrp;

      if (LstGrpsIWant.NumGrps)	// If I have selected groups...
        {
         /***** Create a list of groups selected from myGroups *****/
         if ((LstGrpsIWant.GrpCods = calloc (LstGrpsIWant.NumGrps,
                                             sizeof (*LstGrpsIWant.GrpCods))) == NULL)
	    Err_NotEnoughMemoryExit ();
         for (NumGrp = 0, Ptr = myGroups;
              *Ptr;
              NumGrp++)
	   {
	    /* Find next string in text until comma (leading and trailing spaces are removed) */
            Str_GetNextStringUntilComma (&Ptr,LongStr,Cns_MAX_DIGITS_LONG);
	    LstGrpsIWant.GrpCods[NumGrp] = Str_ConvertStrCodToLongCod (LongStr);
	   }
        }
     }

   /***** Change my groups *****/
   SendMyGroupsOut->success = Grp_ChangeGrpsAtomically (Usr_ME,&LstGrpsIWant) == Err_SUCCESS ? 1 :
											       0;

   /***** Free memory with the list of groups which I want to belong to *****/
   Grp_FreeListCodGrp (&LstGrpsIWant);

   /***** Query groups in a course from database *****/
   NumGrps = Grp_DB_GetGrpsInCrs (&mysql_res,courseCode);

   SendMyGroupsOut->numGroups = (int) NumGrps;
   SendMyGroupsOut->groupsArray.__size = (int) NumGrps;

   if (NumGrps == 0)
      SendMyGroupsOut->groupsArray.__ptr = NULL;
   else	// Groups found
     {
      SendMyGroupsOut->groupsArray.__ptr = soap_malloc (soap,
							(SendMyGroupsOut->groupsArray.__size) *
							sizeof (*(SendMyGroupsOut->groupsArray.__ptr)));

      for (NumGrp = 0;
	   NumGrp < NumGrps;
	   NumGrp++)
	{
	 /* Get next group */
	 row = mysql_fetch_row (mysql_res);

	 /* Get group type code (row[0]) */
	 SendMyGroupsOut->groupsArray.__ptr[NumGrp].groupTypeCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get group type name (row[1]) */
         SendMyGroupsOut->groupsArray.__ptr[NumGrp].groupTypeName =
            soap_malloc (soap,Grp_MAX_BYTES_GROUP_TYPE_NAME + 1);
	 Str_Copy (SendMyGroupsOut->groupsArray.__ptr[NumGrp].groupTypeName,row[1],
	           Grp_MAX_BYTES_GROUP_TYPE_NAME);

         /* Get group code (row[2]) */
	 GrpCod = Str_ConvertStrCodToLongCod (row[2]);
	 SendMyGroupsOut->groupsArray.__ptr[NumGrp].groupCode = (int) GrpCod;

         /* Get group name (row[3]) */
         SendMyGroupsOut->groupsArray.__ptr[NumGrp].groupName =
            soap_malloc (soap,Grp_MAX_BYTES_GROUP_NAME + 1);
	 Str_Copy (SendMyGroupsOut->groupsArray.__ptr[NumGrp].groupName,row[3],
	           Grp_MAX_BYTES_GROUP_NAME);

         /* Get max number of students of group (row[4]) and number of current students */
         MaxStudents = Grp_ConvertToNumMaxStdsGrp (row[4]);
         SendMyGroupsOut->groupsArray.__ptr[NumGrp].maxStudents = MaxStudents > Grp_MAX_STUDENTS_IN_A_GROUP ? -1 :
                                                                                                              (int) MaxStudents;

         /* Get number of current students */
         SendMyGroupsOut->groupsArray.__ptr[NumGrp].numStudents = (int) Grp_DB_CountNumUsrsInGrp (Rol_STD,GrpCod);

         /* Get whether group is open ('Y') or closed ('N') (row[5])
            and whether group have file zones ('Y') or not ('N') (row[6]) */
         SendMyGroupsOut->groupsArray.__ptr[NumGrp].open      = row[5][0] == 'Y' ? 1 :
                                                                                   0;
         SendMyGroupsOut->groupsArray.__ptr[NumGrp].fileZones = row[6][0] == 'Y' ? 1 :
                                                                                   0;

         /* Get whether I belong to this group or not */
         SendMyGroupsOut->groupsArray.__ptr[NumGrp].member = Grp_GetIfIBelongToGrp (GrpCod) == Usr_BELONG ? 1 :
													    0;
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/*********** Copy data of a user from database to soap structure *************/
/*****************************************************************************/

static void API_CopyUsrData (struct soap *soap,
			     struct swad__user *Usr,struct Usr_Data *UsrDat,
			     Usr_Can_t ICanSeeUsrID)
  {
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   const char *FirstID;
   size_t Length;

   /* Copy user's code */
   Usr->userCode = UsrDat->UsrCod;

   /* Copy user's nickname */
   Length = strlen (UsrDat->Nickname);
   Usr->userNickname = soap_malloc (soap,Length + 1);
   Str_Copy (Usr->userNickname,UsrDat->Nickname,Length);

   /* Copy user's first ID */
   if (ICanSeeUsrID == Usr_CAN && UsrDat->IDs.List)
      FirstID = UsrDat->IDs.List[0].ID;
   else	// Hide user's ID
      FirstID = "********";
   Length = strlen (FirstID);
   Usr->userID = soap_malloc (soap,Length + 1);
   Str_Copy (Usr->userID,FirstID,Length);

   /* Copy user's surname1 */
   Length = strlen (UsrDat->Surname1);
   Usr->userSurname1 = soap_malloc (soap,Length + 1);
   Str_Copy (Usr->userSurname1,UsrDat->Surname1,Length);

   /* Copy user's surname2 */
   Length = strlen (UsrDat->Surname2);
   Usr->userSurname2 = soap_malloc (soap,Length + 1);
   Str_Copy (Usr->userSurname2,UsrDat->Surname2,Length);

   /* Copy user's first name */
   Length = strlen (UsrDat->FrstName);
   Usr->userFirstname = soap_malloc (soap,Length + 1);
   Str_Copy (Usr->userFirstname,UsrDat->FrstName,Length);

   /* User's photo URL */
   PhotoExists = Pho_BuildLinkToPhoto (UsrDat,PhotoURL);
   Length = strlen (PhotoURL);
   Usr->userPhoto = soap_malloc (soap,Length + 1);
   Str_Copy (Usr->userPhoto,PhotoURL,Length);
  }

/*****************************************************************************/
/***************** Return list of attendance events in a course **************/
/*****************************************************************************/

int swad__getAttendanceEvents (struct soap *soap,
                               char *wsKey,int courseCode,					// input
                               struct swad__getAttendanceEventsOutput *getAttendanceEventsOut)	// output
  {
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumAttEvents;
   unsigned NumAttEvent;
   struct Att_Event Event;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   size_t Length;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getAttendanceEvents;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must be a teacher");

   /***** Query list of attendance events *****/
   NumAttEvents = Att_DB_GetAllEventsData (&mysql_res,courseCode);

   getAttendanceEventsOut->eventsArray.__size =
   getAttendanceEventsOut->numEvents          = (int) NumAttEvents;

   if (getAttendanceEventsOut->numEvents == 0)
      getAttendanceEventsOut->eventsArray.__ptr = NULL;
   else	// Events found
     {
      getAttendanceEventsOut->eventsArray.__ptr = soap_malloc (soap,
							       (getAttendanceEventsOut->eventsArray.__size) *
							       sizeof (*(getAttendanceEventsOut->eventsArray.__ptr)));

      for (NumAttEvent = 0;
	   NumAttEvent < NumAttEvents;
	   NumAttEvent++)
	{
	 /* Get next group */
	 row = mysql_fetch_row (mysql_res);

	 /* Get attendance event (except Txt) */
	 Att_GetEventDataFromRow (row,&Event);

         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].attendanceEventCode = (int) Event.AttCod;
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].hidden = Event.Hidden == HidVis_HIDDEN ? 1 :
										                         0;
         Gbl.Usrs.Other.UsrDat.UsrCod = Event.UsrCod;
	 switch (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,
					       Gbl.Hierarchy.Node[Hie_CRS].HieCod))	// Get some user's data from database
	   {
	    case Exi_EXISTS:
	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname1);
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname1 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname1,
			 Gbl.Usrs.Other.UsrDat.Surname1,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname2);
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname2 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname2,
			 Gbl.Usrs.Other.UsrDat.Surname2,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.FrstName);
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userFirstname =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userFirstname,
			 Gbl.Usrs.Other.UsrDat.FrstName,Length);

	       PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
	       Length = strlen (PhotoURL);
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto,
			 PhotoURL,Length);
	       break;
	    case Exi_DOES_NOT_EXIST:
	    default:
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname1  = NULL;
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname2  = NULL;
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userFirstname = NULL;
	       getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto     = NULL;
	       break;
	   }

         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].startTime = (int) Event.TimeUTC[Dat_STR_TIME];
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].endTime   = (int) Event.TimeUTC[Dat_END_TIME];
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].commentsTeachersVisible = Event.ClosedOrOpen == CloOpe_OPEN ? 1 :
															      0;
         Length = strlen (Event.Title);
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].title =
            soap_malloc (soap,Length + 1);
         Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].title,
                   Event.Title,Length);

	 /* Get Txt (row[9]) */
         Length = strlen (row[9]);
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].text =
            soap_malloc (soap,Length + 1);
         Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].text,
                   row[9],Length);

	 /* Get list of groups for this attendance event */
	 API_GetListGrpsInAttendanceEventFromDB (soap,
						 Event.AttCod,
						 &(getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].groups));
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/**************** Get lists of groups of an attendance event *****************/
/*****************************************************************************/

static void API_GetListGrpsInAttendanceEventFromDB (struct soap *soap,
						    long AttCod,char **ListGroups)
  {
   MYSQL_RES *mysql_res;
   long NumGrps;
   long NumGrp;
   long GrpCod;
   char GrpCodStr[Cns_MAX_DIGITS_LONG + 1];
   size_t Length;

   /***** Get list of groups *****/
   if ((NumGrps = Att_DB_GetGrpCodsAssociatedToEvent (&mysql_res,AttCod)))	// Events found
     {
      Length = NumGrps * (10 + 1) - 1;
      *ListGroups = soap_malloc (soap,Length + 1);
      (*ListGroups)[0] = '\0';

      for (NumGrp = 0;
	   NumGrp < NumGrps;
	   NumGrp++)
	{
	 /* Get next group */
	 GrpCod = DB_GetNextCode (mysql_res);
	 snprintf (GrpCodStr,sizeof (GrpCodStr),NumGrp ? ",%ld" :
			                                 "%ld",
		   GrpCod);
	 Str_Concat (*ListGroups,GrpCodStr,Length);
	}
     }
   else
      *ListGroups = NULL;

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/****************** Send the data of an attendance event *********************/
/*****************************************************************************/

int swad__sendAttendanceEvent (struct soap *soap,
                               char *wsKey,int attendanceEventCode,int courseCode,int hidden,
                               int startTime,int endTime,int commentsTeachersVisible,
                               char *title,char *text,char *groups,				// input
                               struct swad__sendAttendanceEventOutput *sendAttendanceEventOut)	// output
  {
   int ReturnCode;
   struct Att_Event Event;
   OldNew_OldNew_t OldNewEvent;
   __attribute__((unused)) Exi_Exist_t AttExists;
   static void (*CreateUpdate[OldNew_NUM_OLD_NEW]) (struct Att_Event *Event,const char *Description) =
     {
      [OldNew_OLD] = Att_UpdateEvent,
      [OldNew_NEW] = Att_CreateEvent,
     };

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_sendAttendanceEvent;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must be a teacher");

   /**** Get data of attendance event *****/
   /* Event code */
   Event.AttCod = (long) attendanceEventCode;

   /* Course code */
   OldNewEvent = Event.AttCod > 0 ? OldNew_OLD :	// The event already exists
				    OldNew_NEW;
   switch (OldNewEvent)
     {
      case OldNew_OLD:
	 AttExists = Att_GetEventDataByCod (&Event);
	 if (Event.HieCod != (long) courseCode)
	    return soap_receiver_fault (soap,
					"Request forbidden",
					"Attendance event does not belong to course");
	 break;
      case OldNew_NEW:
      default:
	 Event.HieCod = (long) courseCode;
	 break;
     }

   /* Is event hidden or visible? */
   Event.Hidden = (hidden ? HidVis_HIDDEN :
	                             HidVis_VISIBLE);

   /* User's code (really not used) */
   Event.UsrCod = Gbl.Usrs.Me.UsrDat.UsrCod;

   /* startTime and endTime */
   Event.TimeUTC[Dat_STR_TIME] = (time_t) startTime;
   Event.TimeUTC[Dat_END_TIME] = (time_t) endTime;

   /* Are teacher's comments hidden or visible? */
   Event.CommentTchVisible = (commentsTeachersVisible ? HidVis_VISIBLE :
	                                                HidVis_HIDDEN);

   /* Title */
   if (!title[0])
      return soap_receiver_fault (soap,
				  "Request forbidden",
				  "Title of attendance event is empty");
   Str_Copy (Event.Title,title,sizeof (Event.Title) - 1);

   /* Create a list of groups selected */
   API_GetLstGrpsSel (groups);

   /***** Create or update attendance event *****/
   CreateUpdate[OldNewEvent] (&Event,text);

   /***** Free memory for list of selected groups *****/
   Grp_FreeListCodSelectedGrps ();

   sendAttendanceEventOut->attendanceEventCode = Event.AttCod;

   return SOAP_OK;
  }

/*****************************************************************************/
/************************ Remove an attendance event *************************/
/*****************************************************************************/

int swad__removeAttendanceEvent (struct soap *soap,
                                 char *wsKey,int attendanceEventCode,					// input
                                 struct swad__removeAttendanceEventOutput *removeAttendanceEventOut)	// output
  {
   int ReturnCode;
   struct Att_Event Event;
   __attribute__((unused)) Exi_Exist_t AttExists;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_removeAttendanceEvent;
   removeAttendanceEventOut->attendanceEventCode = 0;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /**** Get data of attendance event *****/
   /* Event code */
   Event.AttCod = (long) attendanceEventCode;

   /* Course code */
   if (Event.AttCod > 0)	// The event already exists
     {
      AttExists = Att_GetEventDataByCod (&Event);
      Gbl.Hierarchy.Node[Hie_CRS].HieCod = Event.HieCod;
     }
   else
      return soap_receiver_fault (soap,
				  "Request forbidden",
				  "Attendance event does not exist");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must be a teacher");

   /***** Remove the attendance event from database *****/
   Att_RemoveEventFromDB (Event.AttCod);

   removeAttendanceEventOut->attendanceEventCode = Event.AttCod;

   return SOAP_OK;
  }

/*****************************************************************************/
/********************** Create a list of groups selected *********************/
/*****************************************************************************/

static void API_GetLstGrpsSel (const char *Groups)
  {
   const char *Ptr;
   char LongStr[Cns_MAX_DIGITS_LONG + 1];
   unsigned NumGrp;

   /***** Count number of groups *****/
   for (Ptr = Groups, NumGrp = 0;
	*Ptr;
	NumGrp++)
      Str_GetNextStringUntilComma (&Ptr,LongStr,Cns_MAX_DIGITS_LONG);
   Gbl.Crs.Grps.LstGrpsSel.NumGrps = NumGrp;

   /***** Create a list of groups selected *****/
   if (Gbl.Crs.Grps.LstGrpsSel.NumGrps)
     {
      // Here NestedCalls is always 0
      Gbl.Crs.Grps.LstGrpsSel.NestedCalls++;
      // Here NestedCalls is always 1
      if ((Gbl.Crs.Grps.LstGrpsSel.GrpCods = calloc (Gbl.Crs.Grps.LstGrpsSel.NumGrps,
                                                     sizeof (*Gbl.Crs.Grps.LstGrpsSel.GrpCods))) == NULL)
	 Err_NotEnoughMemoryExit ();

      for (Ptr = Groups, NumGrp = 0;
	   *Ptr;
	   )
	{
	 Str_GetNextStringUntilComma (&Ptr,LongStr,Cns_MAX_DIGITS_LONG);
	 Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrp] = Str_ConvertStrCodToLongCod (LongStr);
	 if (Grp_DB_CheckIfGrpExistsInCrs (Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrp],
					   Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_EXISTS)
	    NumGrp++;
	}
      Gbl.Crs.Grps.LstGrpsSel.NumGrps = NumGrp;	// Update number of groups
     }
  }

/*****************************************************************************/
/*********** Return a list with the users in an attendance event *************/
/*****************************************************************************/

int swad__getAttendanceUsers (struct soap *soap,
                              char *wsKey,int attendanceEventCode,				// input
                              struct swad__getAttendanceUsersOutput *getAttendanceUsersOut)	// output
  {
   int ReturnCode;
   struct Att_Event Event;
   __attribute__((unused)) Exi_Exist_t AttExists;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumUsrs;
   unsigned NumUsr;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   size_t Length;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getAttendanceUsers;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get course of this attendance event *****/
   Event.AttCod = (long) attendanceEventCode;
   AttExists = Att_GetEventDataByCod (&Event);
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = Event.HieCod;

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must be a teacher");

   /***** Query list of attendance users *****/
   NumUsrs = Att_DB_GetListUsrsInEvent (&mysql_res,Event.AttCod);

   getAttendanceUsersOut->numUsers          =
   getAttendanceUsersOut->usersArray.__size = (int) NumUsrs;

   if (NumUsrs)	// Users found
     {
      getAttendanceUsersOut->usersArray.__ptr = soap_malloc (soap,
							     (getAttendanceUsersOut->usersArray.__size) *
							     sizeof (*(getAttendanceUsersOut->usersArray.__ptr)));

      for (NumUsr = 0;
	   NumUsr < NumUsrs;
	   NumUsr++)
	{
	 /* Get next user */
	 row = mysql_fetch_row (mysql_res);

	 /* Get user's code (row[0]) */
	 Gbl.Usrs.Other.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[0]);
         getAttendanceUsersOut->usersArray.__ptr[NumUsr].userCode = (int) Gbl.Usrs.Other.UsrDat.UsrCod;

         /* Get user's data */
	 switch (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,-1L))	// Get some user's data from database
	   {
	    case Exi_EXISTS:
	       Length = strlen (Gbl.Usrs.Other.UsrDat.Nickname);
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userNickname =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumUsr].userNickname,
			 Gbl.Usrs.Other.UsrDat.Nickname,Length);

	       if (Gbl.Usrs.Other.UsrDat.IDs.Num)
		 {
		  Length = strlen (Gbl.Usrs.Other.UsrDat.IDs.List[0].ID);	// TODO: What user's ID?
		  getAttendanceUsersOut->usersArray.__ptr[NumUsr].userID =
		     soap_malloc (soap,Length + 1);
		  Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumUsr].userID,
			    Gbl.Usrs.Other.UsrDat.IDs.List[0].ID,Length);
		 }
	       else
		 {
		  getAttendanceUsersOut->usersArray.__ptr[NumUsr].userID =
		     soap_malloc (soap,1);
		  getAttendanceUsersOut->usersArray.__ptr[NumUsr].userID[0] = '\0';
		 }

	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname1);
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userSurname1 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumUsr].userSurname1,
			 Gbl.Usrs.Other.UsrDat.Surname1,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname2);
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userSurname2 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumUsr].userSurname2,
			 Gbl.Usrs.Other.UsrDat.Surname2,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.FrstName);
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userFirstname =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumUsr].userFirstname,
			 Gbl.Usrs.Other.UsrDat.FrstName,Length);

	       PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
	       Length = strlen (PhotoURL);
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userPhoto =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumUsr].userPhoto,
			 PhotoURL,Length);
	       break;
	    case Exi_DOES_NOT_EXIST:
	    default:
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userNickname  = NULL;
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userID        = NULL;
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userSurname1  = NULL;
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userSurname2  = NULL;
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userFirstname = NULL;
	       getAttendanceUsersOut->usersArray.__ptr[NumUsr].userPhoto     = NULL;
	       break;
	   }

	 /* Get if user is present or not (row[1]) */
	 getAttendanceUsersOut->usersArray.__ptr[NumUsr].present = row[1][0] == 'Y' ? 1 :
										      0;
	}
     }
   else
      getAttendanceUsersOut->usersArray.__ptr = NULL;

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/************ Send a list with the users in an attendance event **************/
/*****************************************************************************/

int swad__sendAttendanceUsers (struct soap *soap,
                               char *wsKey,int attendanceEventCode,
                               char *users,int setOthersAsAbsent,				// input
                               struct swad__sendAttendanceUsersOutput *sendAttendanceUsersOut)	// output
  {
   int ReturnCode;
   struct Att_Event Event;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_sendAttendanceUsers;

   /***** Initialize output *****/
   sendAttendanceUsersOut->success = 0;
   sendAttendanceUsersOut->numUsers = 0;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get course of this attendance event *****/
   Event.AttCod = (long) attendanceEventCode;
   if (Att_GetEventDataByCod (&Event) == Exi_DOES_NOT_EXIST)
      return SOAP_OK;	// return with success = 0
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = Event.HieCod;

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must be a teacher");

   /***** Set users as present *****/
   Att_DB_SetUsrsAsPresent (Event.AttCod,users,setOthersAsAbsent);

   /***** Purge absent users without comments from table *****/
   if (setOthersAsAbsent)
      Att_DB_RemoveUsrsAbsentWithoutCommentsFromEvent (Event.AttCod);

   sendAttendanceUsersOut->success = 1;

   return SOAP_OK;
  }

/*****************************************************************************/
/********************* Return notifications of a user ************************/
/*****************************************************************************/

int swad__getNotifications (struct soap *soap,
                            char *wsKey,long beginTime,					// input
                            struct swad__getNotificationsOutput *getNotificationsOut)	// output
  {
   extern Err_SuccessOrError_t (*Hie_GetDataByCod[Hie_NUM_LEVELS]) (struct Hie_Node *Node);
   extern const char *Ntf_WSNotifyEvents[Ntf_NUM_NOTIFY_EVENTS];
   extern const char *Txt_Forum;
   extern const char *Txt_HIERARCHY_SINGUL_Abc[Hie_NUM_LEVELS];
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumNotifs;
   unsigned NumNotif;
   long NtfCod;
   Ntf_NotifyEvent_t NotifyEvent;
   long EventTime;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   struct Hie_Node Hie[Hie_NUM_LEVELS];
   long Cod;
   __attribute__((unused)) Err_SuccessOrError_t SuccessOrError;
   struct For_Forum ForumSelected;
   char ForumName[For_MAX_BYTES_FORUM_NAME + 1];
   char SummaryStr[Ntf_MAX_BYTES_SUMMARY + 1];
   char *ContentStr;
   Ntf_Status_t Status;
   size_t Length;
   Hie_Level_t L;
   unsigned Col;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getNotifications;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     -1L) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Get my language from database *****/
   if ((ReturnCode = API_GetMyLanguage (soap)) != SOAP_OK)
      return ReturnCode;

   /***** Get my notifications from database *****/
   if ((NumNotifs = Ntf_DB_GetMyRecentNotifications (&mysql_res,(time_t) beginTime)))	// Notifications found
     {
      getNotificationsOut->numNotifications          =
      getNotificationsOut->notificationsArray.__size = (int) NumNotifs;
      getNotificationsOut->notificationsArray.__ptr = soap_malloc (soap,
								   (getNotificationsOut->notificationsArray.__size) *
								   sizeof (*(getNotificationsOut->notificationsArray.__ptr)));

      for (NumNotif = 0;
	   NumNotif < NumNotifs;
	   NumNotif++)
	{
	 /* Get next notification */
	 row = mysql_fetch_row (mysql_res);

         /* Get unique notification code (row[0]) */
         NtfCod = Str_ConvertStrCodToLongCod (row[0]);
         getNotificationsOut->notificationsArray.__ptr[NumNotif].notifCode = (int) NtfCod;

         /* Get notification event type (row[1]) */
         NotifyEvent = Ntf_GetNotifyEventFromStr (row[1]);
         getNotificationsOut->notificationsArray.__ptr[NumNotif].eventType =
            soap_malloc (soap,Ntf_MAX_BYTES_NOTIFY_EVENT + 1);
         Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].eventType,
                   Ntf_WSNotifyEvents[NotifyEvent],Ntf_MAX_BYTES_NOTIFY_EVENT);

         /* Get time of the event (row[2]) */
         EventTime = 0L;
         if (row[2])
            sscanf (row[2],"%ld",&EventTime);
         getNotificationsOut->notificationsArray.__ptr[NumNotif].eventTime = EventTime;

         /* Get user's code of the user who caused the event (row[3]) */
         Gbl.Usrs.Other.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[3]);

	 /* Get institution code, center code, degree code and course code
	    (row[4], row[5], row[6] and row[7]) */
	 for (L  = Hie_INS, Col = 4;
	      L <= Hie_CRS;
	      L++, Col++)
	   {
	    Hie[L].HieCod = Str_ConvertStrCodToLongCod (row[Col]);
	    SuccessOrError = Hie_GetDataByCod[L] (&Hie[L]);
	   }

	 switch (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,
					       Hie[Hie_CRS].HieCod))	// Get some user's data from database
	   {
	    case Exi_EXISTS:
	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userNickname =
		  soap_malloc (soap,Nck_MAX_BYTES_NICK_WITHOUT_ARROBA + 1);
	       Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userNickname,
			 Gbl.Usrs.Other.UsrDat.Nickname,
			 Nck_MAX_BYTES_NICK_WITHOUT_ARROBA);

	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userSurname1 =
		  soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
	       Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userSurname1,
			 Gbl.Usrs.Other.UsrDat.Surname1,
			 Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userSurname2 =
		  soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
	       Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userSurname2,
			 Gbl.Usrs.Other.UsrDat.Surname2,
			 Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userFirstname =
		  soap_malloc (soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
	       Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userFirstname,
			 Gbl.Usrs.Other.UsrDat.FrstName,
			 Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

	       PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userPhoto =
		  soap_malloc (soap,WWW_MAX_BYTES_WWW + 1);
	       Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userPhoto,
			 PhotoURL,WWW_MAX_BYTES_WWW);
	       break;
	    case Exi_DOES_NOT_EXIST:
	    default:
	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userNickname  = NULL;
	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userSurname1  = NULL;
	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userSurname2  = NULL;
	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userFirstname = NULL;
	       getNotificationsOut->notificationsArray.__ptr[NumNotif].userPhoto     = NULL;
	       break;
	   }

         /* Get message/post/... code (row[8]) */
         Cod = Str_ConvertStrCodToLongCod (row[8]);
         getNotificationsOut->notificationsArray.__ptr[NumNotif].eventCode = (int) Cod;

         /* Set location */
         getNotificationsOut->notificationsArray.__ptr[NumNotif].location =
            soap_malloc (soap,Ntf_MAX_BYTES_NOTIFY_LOCATION + 1);

	 if (NotifyEvent == Ntf_EVENT_FORUM_POST_COURSE ||
	     NotifyEvent == Ntf_EVENT_FORUM_REPLY)
	   {
	    For_GetThreadForumTypeAndHieCodOfAPost (Cod,&ForumSelected);
            For_SetForumName (&ForumSelected,ForumName,
        	              Gbl.Prefs.Language,For_DONT_USE_HTML_ENTITIES);	// Set forum name in recipient's language
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"%s: %s",
                     Txt_Forum,ForumName);
           }
         else
           {
	    for (L  = Hie_CRS;
		 L >= Hie_INS;
		 L--)
	       if (Hie[L].HieCod > 0)
		 {
		  sprintf (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"%s: %s",
			   Txt_HIERARCHY_SINGUL_Abc[L],Hie[L].ShrtName);
		  break;
		 }
	    if (L < Hie_INS)
	       Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"-",
			 Ntf_MAX_BYTES_NOTIFY_LOCATION);
           }

         /* Get status (row[9]) */
         if (sscanf (row[9],"%u",&Status) != 1)
            Status = (Ntf_Status_t) 0;
         getNotificationsOut->notificationsArray.__ptr[NumNotif].status = (int) Status;

         /* Get summary and content */
         ContentStr = NULL;
         Ntf_GetNotifSummaryAndContent (SummaryStr,&ContentStr,NotifyEvent,
                                        Cod,Hie[Hie_CRS].HieCod,Gbl.Usrs.Me.UsrDat.UsrCod,
                                        Ntf_GET_CONTENT);

         Length = strlen (SummaryStr);
         getNotificationsOut->notificationsArray.__ptr[NumNotif].summary =
            soap_malloc (soap,Length + 1);
         Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].summary,
                   SummaryStr,Length);

         if (ContentStr == NULL)
           {
            getNotificationsOut->notificationsArray.__ptr[NumNotif].content =
               soap_malloc (soap,1);
            getNotificationsOut->notificationsArray.__ptr[NumNotif].content[0] = '\0';
           }
         else
           {
            Length = strlen (ContentStr);
            getNotificationsOut->notificationsArray.__ptr[NumNotif].content =
               soap_malloc (soap,Length + 1);
            Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].content,
                      ContentStr,Length);

            /* Free memory used by content string */
            free (ContentStr);
            ContentStr = NULL;
           }
	}
     }
   else	// No notifications found
     {
      getNotificationsOut->numNotifications = 0;
      getNotificationsOut->notificationsArray.__size = 0;
      getNotificationsOut->notificationsArray.__ptr = NULL;
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/********************* Get my language from database *************************/
/*****************************************************************************/

static int API_GetMyLanguage (struct soap *soap)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   Exi_Exist_t UsrExists;

   /***** Get user's language *****/
   UsrExists = Set_DB_GetMyLanguage (&mysql_res);

   /***** Get language from database *****/
   if (UsrExists == Exi_EXISTS)
     {
      row = mysql_fetch_row (mysql_res);

      /* Get language (row[0]) */
      Gbl.Prefs.Language = Lan_GetLanguageFromStr (row[0]);
      if (Gbl.Prefs.Language == Lan_LANGUAGE_UNKNOWN)	// Language stored in database is unknown
	 Gbl.Prefs.Language = Cfg_DEFAULT_LANGUAGE;
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   if (UsrExists == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's language from database",
	                          "User doen't exist in database");

   return SOAP_OK;
  }

/*****************************************************************************/
/************************* Send a notice to a course *************************/
/*****************************************************************************/

int swad__markNotificationsAsRead (struct soap *soap,
                                   char *wsKey,char *notifications,						// input
                                   struct swad__markNotificationsAsReadOutput *markNotificationsAsReadOut)	// output
  {
   int ReturnCode;
   const char *Ptr;
   char LongStr[Cns_MAX_DIGITS_LONG + 1];
   unsigned NumNtf;
   unsigned NumNtfsMarkedAsRead = 0;
   long NtfCod;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_markNotificationsAsRead;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     -1L) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   if (notifications[0])
     {
      /***** Mark notifications from list as read *****/
      for (NumNtf = 0, Ptr = notifications;
	   *Ptr;
	   NumNtf++)
	{
	 /* Find next string in text until comma (leading and trailing spaces are removed) */
	 Str_GetNextStringUntilComma (&Ptr,LongStr,Cns_MAX_DIGITS_LONG);
         if ((NtfCod = Str_ConvertStrCodToLongCod (LongStr)) > 0)
           {
	    /***** Mark notification as read in the database *****/
	    Ntf_DB_MarkNotifAsSeenUsingNtfCod (NtfCod);
	    NumNtfsMarkedAsRead++;
           }
        }
     }

   /***** Return notification code *****/
   markNotificationsAsReadOut->numNotifications = (int) NumNtfsMarkedAsRead;

   return SOAP_OK;
  }

/*****************************************************************************/
/****************** Send a message to one or more users **********************/
/*****************************************************************************/

#define API_MAX_BYTES_QUERY_RECIPIENTS (10 * 1024 - 1)

int swad__sendMessage (struct soap *soap,
                       char *wsKey,int messageCode,char *to,char *subject,char *body,	// input
                       struct swad__sendMessageOutput *sendMessageOut)			// output
  {
   int ReturnCode;
   long ReplyUsrCod = -1L;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumUsrs;
   unsigned NumUsr;
   bool NotifyByEmail;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_sendMessage;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     -1L) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if the message is a reply to a previous message *****/
   if (messageCode)
     {
      /***** Check if the original message was really received by me *****/
      if (!Msg_DB_CheckIfMsgHasBeenReceivedByMe ((long) messageCode))
         return soap_sender_fault (soap,
                                   "Can not send reply message",
                                   "Original message does not exist");

      /***** Get the recipient of the message *****/
      if ((ReplyUsrCod = Msg_DB_GetSender ((long) messageCode)) <= 0)
         return soap_sender_fault (soap,
                                   "Can not send reply message",
                                   "Original message does not exist");
     }

   /***** Query database to get users *****/
   NumUsrs = Msg_DB_GetRecipientsCods (&mysql_res,ReplyUsrCod,to);

   /***** Initialize output structure *****/
   sendMessageOut->numUsers          =
   sendMessageOut->usersArray.__size = (int) NumUsrs;
   sendMessageOut->usersArray.__ptr  = NULL;

   /***** Get users *****/
   if (NumUsrs)	// There are a recipient to reply or nicknames in "to"
     {
      sendMessageOut->usersArray.__ptr = soap_malloc (soap,
						      (sendMessageOut->usersArray.__size) *
						      sizeof (*(sendMessageOut->usersArray.__ptr)));

      for (NumUsr = 0;
	   NumUsr < NumUsrs;
	   NumUsr++)
	{
	 /* Get next user */
	 row = mysql_fetch_row (mysql_res);

	 /* Get user's code (row[0]) */
	 if ((Gbl.Usrs.Other.UsrDat.UsrCod = (long) Str_ConvertStrCodToLongCod (row[0])) > 0)
	    /* Get recipient data */
	    if (Usr_ChkUsrCodAndGetAllUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,
							 Usr_DONT_GET_PREFS,
							 Usr_DONT_GET_ROLE_IN_CRS) == Exi_EXISTS)
	      {
	       /* This received message must be notified by email? */
	       NotifyByEmail = (Usr_ItsMe (Gbl.Usrs.Other.UsrDat.UsrCod) == Usr_OTHER &&
				(Gbl.Usrs.Other.UsrDat.NtfEvents.SendEmail & (1 << Ntf_EVENT_MESSAGE)));

	       /* Send message to this user */
	       if ((ReturnCode = API_SendMessageToUsr ((long) messageCode,
						       ReplyUsrCod,
						       Gbl.Usrs.Other.UsrDat.UsrCod,
						       NotifyByEmail,
						       subject,
						       body)) != SOAP_OK)
		 {
		  DB_FreeMySQLResult (&mysql_res);
		  return ReturnCode;
		 }

	       /* Copy user's data into output structure */
	       API_CopyUsrData (soap,
				&(sendMessageOut->usersArray.__ptr[NumUsr]),
				&Gbl.Usrs.Other.UsrDat,
				Usr_CAN_NOT);
	      }
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************* Send a message to one user ************************/
/*****************************************************************************/

static int API_SendMessageToUsr (long OriginalMsgCod,
                                 long ReplyUsrCod,long RecipientUsrCod,
                                 bool NotifyByEmail,
                                 const char *Subject,const char *Content)
  {
   static bool MsgAlreadyInserted = false;
   static long MsgCod;

   /***** Create message *****/
   if (!MsgAlreadyInserted)      // The message is inserted only once in the table of messages sent
     {
      /***** Insert message subject and body in the database *****/
      /* Get the code of the inserted item */
      MsgCod = Msg_DB_CreateNewMsg (Subject,Content,
                                    -1L);	// No media content

      /* Insert message in sent messages */
      Msg_DB_CreateSntMsg (MsgCod,
                           -1L);		// No origin course

      MsgAlreadyInserted = true;
     }

   /***** Insert message received in the database *****/
   Msg_DB_CreateRcvMsg (MsgCod,RecipientUsrCod,NotifyByEmail);

   /***** Create notification for this recipient.
          If this recipient wants to receive notifications by email,
          activate the sending of a notification *****/
   Ntf_DB_StoreNotifyEventToUsr (Ntf_EVENT_MESSAGE,RecipientUsrCod,MsgCod,
				 (Ntf_Status_t) (NotifyByEmail ? Ntf_STATUS_BIT_EMAIL :
								 0),
				 -1L,-1L,-1L,-1L);

   /***** If this recipient is the original sender of a message been replied... *****/
   if (RecipientUsrCod == ReplyUsrCod)
      /***** ...then update received message setting Replied field to true *****/
      Msg_DB_SetRcvMsgAsReplied (OriginalMsgCod);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************* Send a notice to a course *************************/
/*****************************************************************************/

int swad__sendNotice (struct soap *soap,
                      char *wsKey,int courseCode,char *body,		// input
                      struct swad__sendNoticeOutput *sendNoticeOut)	// output
  {
   int ReturnCode;
   long NotCod;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_sendNotice;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (soap,
						   Gbl.Hierarchy.Node[Hie_CRS].HieCod,
						   -1L)) != SOAP_OK)
      return ReturnCode;

   /***** Get degree of current course *****/
   Gbl.Hierarchy.Node[Hie_DEG].HieCod = Crs_DB_GetDegCodOfCourseByCod (Gbl.Hierarchy.Node[Hie_CRS].HieCod);

   /***** Check if I am a teacher *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must be a teacher");

   /***** Insert notice in the database *****/
   /* Get the code of the inserted item */
   NotCod = Not_DB_InsertNotice (body,PriPub_PUBLIC);

   /***** Create notifications *****/
   // TODO: create notifications
   // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

   /***** Return notification code *****/
   sendNoticeOut->noticeCode = (int) NotCod;

   return SOAP_OK;
  }

/*****************************************************************************/
/****************** Return test configuration in a course ********************/
/*****************************************************************************/

#define TstPrn_MAX_BYTES_FEEDBACK_TYPE		  32

int swad__getTestConfig (struct soap *soap,
                         char *wsKey,int courseCode,				// input
                         struct swad__getTestConfigOutput *getTestConfigOut)	// output
  {
   int ReturnCode;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getTestConfig;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get degree of current course *****/
   Gbl.Hierarchy.Node[Hie_DEG].HieCod = Crs_DB_GetDegCodOfCourseByCod (Gbl.Hierarchy.Node[Hie_CRS].HieCod);

   /***** Set default result to empty *****/
   getTestConfigOut->numQuestions =
   getTestConfigOut->minQuestions =
   getTestConfigOut->defQuestions =
   getTestConfigOut->maxQuestions = 0;
   getTestConfigOut->visibility   = TstVis_MIN_VISIBILITY;

   /* TODO: Remove these lines in 2021 */
   getTestConfigOut->feedback = soap_malloc (soap,TstPrn_MAX_BYTES_FEEDBACK_TYPE + 1);
   getTestConfigOut->feedback[0] = '\0';

   /***** Get test configuration *****/
   TstCfg_GetConfig ();
   getTestConfigOut->pluggable = TstCfg_GetConfigPluggable () == TstCfg_PLUGGABLE_YES ? 1 :
	                                                                                0;
   getTestConfigOut->minQuestions = (int) TstCfg_GetConfigMin ();
   getTestConfigOut->defQuestions = (int) TstCfg_GetConfigDef ();
   getTestConfigOut->maxQuestions = (int) TstCfg_GetConfigMax ();
   getTestConfigOut->visibility   = (int) TstCfg_GetConfigVisibility ();

   /* Convert from visibility to old feedback */
   /* TODO: Remove these lines in 2021 */
   if (TstVis_StudentsCanViewTotalScore (TstCfg_GetConfigVisibility ()) == Usr_CAN_NOT)
      Str_Copy (getTestConfigOut->feedback,"nothing",
		TstPrn_MAX_BYTES_FEEDBACK_TYPE);
   else if (TstVis_StudentsCanViewEachQstScore (TstCfg_GetConfigVisibility ()) == Usr_CAN_NOT)
      Str_Copy (getTestConfigOut->feedback,"totalResult",
		TstPrn_MAX_BYTES_FEEDBACK_TYPE);
   else if (TstVis_StudentsCanViewCorrectAns (TstCfg_GetConfigVisibility ()) == Usr_CAN_NOT)
      Str_Copy (getTestConfigOut->feedback,"eachResult",
		TstPrn_MAX_BYTES_FEEDBACK_TYPE);
   else if (TstVis_StudentsCanViewFeedbackTxt (TstCfg_GetConfigVisibility ()) == Usr_CAN_NOT)
      Str_Copy (getTestConfigOut->feedback,"eachGoodBad",
		TstPrn_MAX_BYTES_FEEDBACK_TYPE);
   else
      Str_Copy (getTestConfigOut->feedback,"fullFeedback",
		TstPrn_MAX_BYTES_FEEDBACK_TYPE);

   /***** Get number of tests *****/
   if (TstCfg_GetConfigPluggable () == TstCfg_PLUGGABLE_YES &&
       TstCfg_GetConfigMax () > 0)
      getTestConfigOut->numQuestions = (int) Qst_DB_GetNumQstsInCrs ((long) courseCode);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************* Return tests of a course **************************/
/*****************************************************************************/

int swad__getTests (struct soap *soap,
                    char *wsKey,int courseCode,long beginTime,	// input
                    struct swad__getTestsOutput *getTestsOut)	// output
  {
   int ReturnCode;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getTests;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get degree of current course *****/
   Gbl.Hierarchy.Node[Hie_DEG].HieCod = Crs_DB_GetDegCodOfCourseByCod (Gbl.Hierarchy.Node[Hie_CRS].HieCod);

   /***** Set default result to empty *****/
   getTestsOut->tagsArray.__size         = 0;
   getTestsOut->tagsArray.__ptr          = NULL;
   getTestsOut->questionsArray.__size    = 0;
   getTestsOut->questionsArray.__ptr     = NULL;
   getTestsOut->answersArray.__size      = 0;
   getTestsOut->answersArray.__ptr       = NULL;
   getTestsOut->questionTagsArray.__size = 0;
   getTestsOut->questionTagsArray.__ptr  = NULL;

   /***** Get test configuration *****/
   if (TstCfg_CheckIfPluggableIsUnknownAndCrsHasTests ())
     {
      /***** Get tags *****/
      if ((ReturnCode = API_GetTstTags (soap,
					(long) courseCode,getTestsOut)) != SOAP_OK)
         return ReturnCode;

      /***** Get questions *****/
      if ((ReturnCode = API_GetTstQuestions (soap,
					     (long) courseCode,(time_t) beginTime,getTestsOut)) != SOAP_OK)
         return ReturnCode;

      /***** Get answers *****/
      if ((ReturnCode = API_GetTstAnswers (soap,
					   (long) courseCode,(time_t) beginTime,getTestsOut)) != SOAP_OK)
         return ReturnCode;

      /***** Get tags for each question *****/
      if ((ReturnCode = API_GetTstQuestionTags (soap,
						(long) courseCode,(time_t) beginTime,getTestsOut)) != SOAP_OK)
         return ReturnCode;
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/**** Get test tags (only not hidden) from database giving a course code *****/
/*****************************************************************************/

static int API_GetTstTags (struct soap *soap,
			   long HieCod,struct swad__getTestsOutput *getTestsOut)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumTags;
   unsigned NumTag;

   /***** Get available tags from database *****/
   NumTags = Tag_DB_GetEnabledTagsFromCrs (&mysql_res,-1L,HieCod);

   getTestsOut->tagsArray.__size = (int) NumTags;

   if (NumTags == 0)
      getTestsOut->tagsArray.__ptr = NULL;
   else	// Tags found
     {
      getTestsOut->tagsArray.__ptr = soap_malloc (soap,
						  (getTestsOut->tagsArray.__size) *
						  sizeof (*(getTestsOut->tagsArray.__ptr)));

      for (NumTag = 0;
	   NumTag < NumTags;
	   NumTag++)
	{
	 /* Get next tag */
	 row = mysql_fetch_row (mysql_res);

         /* Get tag code (row[0]) */
	 getTestsOut->tagsArray.__ptr[NumTag].tagCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get tag text (row[1]) */
         getTestsOut->tagsArray.__ptr[NumTag].tagText =
            soap_malloc (soap,Tag_MAX_BYTES_TAG + 1);
	 Str_Copy (getTestsOut->tagsArray.__ptr[NumTag].tagText,row[1],
	           Tag_MAX_BYTES_TAG);
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/******* Get recent test questions from database giving a course code ********/
/*****************************************************************************/

static int API_GetTstQuestions (struct soap *soap,
				long HieCod,time_t BeginTime,
				struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Qst_StrAnswerTypesXML[Qst_NUM_ANS_TYPES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumQsts;
   unsigned NumQst;
   Qst_AnswerType_t AnswerType;

   /***** Get recent test questions from database *****/
   // DISTINCT is necessary to not repeat questions
   NumQsts = Qst_DB_GetRecentQuestions (&mysql_res,HieCod,BeginTime);

   getTestsOut->questionsArray.__size = (int) NumQsts;

   if (NumQsts == 0)
      getTestsOut->questionsArray.__ptr = NULL;
   else	// Questions found
     {
      getTestsOut->questionsArray.__ptr = soap_malloc (soap,
						       (getTestsOut->questionsArray.__size) *
						       sizeof (*(getTestsOut->questionsArray.__ptr)));

      for (NumQst = 0;
	   NumQst < NumQsts;
	   NumQst++)
	{
	 /* Get next question */
	 row = mysql_fetch_row (mysql_res);

         /* Get question code (row[0]) */
	 getTestsOut->questionsArray.__ptr[NumQst].questionCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get answer type (row[1]) */
         AnswerType = Qst_ConvertFromStrAnsTypDBToAnsTyp (row[1]);
         getTestsOut->questionsArray.__ptr[NumQst].answerType =
            soap_malloc (soap,Qst_MAX_BYTES_ANSWER_TYPE + 1);
	 Str_Copy (getTestsOut->questionsArray.__ptr[NumQst].answerType,
	           Qst_StrAnswerTypesXML[AnswerType],
	           Qst_MAX_BYTES_ANSWER_TYPE);

         /* Get shuffle (row[2]) */
         getTestsOut->questionsArray.__ptr[NumQst].shuffle = row[2][0] == 'Y' ? 1 :
                                                                                0;

         /* Get question stem (row[3]) */
         getTestsOut->questionsArray.__ptr[NumQst].stem =
            soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
         Str_Copy (getTestsOut->questionsArray.__ptr[NumQst].stem,row[3],
                   Cns_MAX_BYTES_TEXT);

         /* Get question feedback (row[4]) */
         getTestsOut->questionsArray.__ptr[NumQst].feedback =
            soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
         Str_Copy (getTestsOut->questionsArray.__ptr[NumQst].feedback,row[4],
                   Cns_MAX_BYTES_TEXT);
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/** Get answers of recent test questions from database giving a course code **/
/*****************************************************************************/

static int API_GetTstAnswers (struct soap *soap,
		              long HieCod,time_t BeginTime,
			      struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Qst_StrAnswerTypesXML[Qst_NUM_ANS_TYPES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumAnss;
   unsigned NumAns;
   unsigned Index;

   /***** Get answers to recent test questions from database *****/
   NumAnss = Qst_DB_GetRecentAnswers (&mysql_res,HieCod,BeginTime);

   getTestsOut->answersArray.__size = (int) NumAnss;

   if (NumAnss == 0)
      getTestsOut->answersArray.__ptr = NULL;
   else	// Answers found
     {
      getTestsOut->answersArray.__ptr = soap_malloc (soap,
						     (getTestsOut->answersArray.__size) *
						     sizeof (*(getTestsOut->answersArray.__ptr)));

      for (NumAns = 0;
	   NumAns < NumAnss;
	   NumAns++)
	{
	 /* Get next question */
	 row = mysql_fetch_row (mysql_res);

         /* Get question code (row[0]) */
	 getTestsOut->answersArray.__ptr[NumAns].questionCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get answer index (row[1]) */
         if (sscanf (row[1],"%u",&Index) == 1)
            getTestsOut->answersArray.__ptr[NumAns].answerIndex = (int) Index;
         else
            getTestsOut->answersArray.__ptr[NumAns].answerIndex = 0;	// error

         /* Get correct (row[2]) */
         getTestsOut->answersArray.__ptr[NumAns].correct = row[2][0] == 'Y' ? 1 :
                                                                              0;

         /* Get answer (row[3]) */
         getTestsOut->answersArray.__ptr[NumAns].answerText =
            soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
         Str_Copy (getTestsOut->answersArray.__ptr[NumAns].answerText,
                   row[3],Cns_MAX_BYTES_TEXT);

         /* Get feedback (row[4]) */
         getTestsOut->answersArray.__ptr[NumAns].answerFeedback =
            soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
         Str_Copy (getTestsOut->answersArray.__ptr[NumAns].answerFeedback,
                   row[4],Cns_MAX_BYTES_TEXT);
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/*** Get tags of recent test questions from database giving a course code ****/
/*****************************************************************************/

static int API_GetTstQuestionTags (struct soap *soap,
		                   long HieCod,time_t BeginTime,
				   struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Qst_StrAnswerTypesXML[Qst_NUM_ANS_TYPES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumQstTags;
   unsigned NumQstTag;
   unsigned Index;

   /***** Get recent test questions from database *****/
   NumQstTags = Tag_DB_GetRecentTags (&mysql_res,HieCod,BeginTime);

   getTestsOut->questionTagsArray.__size = (int) NumQstTags;

   if (NumQstTags == 0)
      getTestsOut->questionTagsArray.__ptr = NULL;
   else	// Questions-tags found
     {
      getTestsOut->questionTagsArray.__ptr = soap_malloc (soap,
							  (getTestsOut->questionTagsArray.__size) *
							  sizeof (*(getTestsOut->questionTagsArray.__ptr)));

      for (NumQstTag = 0;
	   NumQstTag < NumQstTags;
	   NumQstTag++)
	{
	 /* Get next question */
	 row = mysql_fetch_row (mysql_res);

         /* Get question code (row[0]) */
	 getTestsOut->questionTagsArray.__ptr[NumQstTag].questionCode = (int) Str_ConvertStrCodToLongCod (row[0]);

         /* Get tag code (row[1]) */
	 getTestsOut->questionTagsArray.__ptr[NumQstTag].tagCode = (int) Str_ConvertStrCodToLongCod (row[1]);

         /* Get tag index (row[2]) */
         if (sscanf (row[2],"%u",&Index) == 1)
            getTestsOut->questionTagsArray.__ptr[NumQstTag].tagIndex = (int) Index;
         else
            getTestsOut->questionTagsArray.__ptr[NumQstTag].tagIndex = 0;	// error
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/***************** Return one test question for Trivial game *****************/
/*****************************************************************************/

int swad__getTrivialQuestion (struct soap *soap,
                              char *wsKey,char *degrees,float lowerScore,float upperScore,	// input
                              struct swad__getTrivialQuestionOutput *getTrivialQuestionOut)	// output
  {
   extern const char *Qst_StrAnswerTypesXML[Qst_NUM_ANS_TYPES];
   int ReturnCode;
   const char *Ptr;
   char LongStr[Cns_MAX_DIGITS_LONG + 1];
   char DegreesStr[API_MAX_BYTES_DEGREES_STR + 1];
   char DegStr[ 1 + 1 + Cns_MAX_DIGITS_LONG + 1 + 1];
   //   DegStr=",   '   - number '  \0"
   long HieCod;	// Degree code
   bool FirstDegree = true;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumAnss;
   unsigned NumAns;
   long QstCod = -1L;
   Qst_AnswerType_t AnswerType;
   unsigned Index;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getTrivialQuestion;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Loop over recipients' nicknames building query *****/
   DegreesStr[0] = '\0';
   for (Ptr = degrees;
        *Ptr;
       )
     {
      /* Find next string in text until comma (leading and trailing spaces are removed) */
      Str_GetNextStringUntilComma (&Ptr,LongStr,Cns_MAX_DIGITS_LONG);

      /* Check if degree code from string is a valid code */
      if (sscanf (LongStr,"%ld",&HieCod) == 1)	// Degree code
	 if (HieCod > 0)
	   {
	    /* Add this degree to query */
	    if (FirstDegree)
	      {
	       snprintf (DegreesStr,sizeof (DegreesStr),"%ld",HieCod);
	       FirstDegree = false;
	      }
	    else
	      {
	       snprintf (DegStr,sizeof (DegStr),",%ld",HieCod);
	       Str_Concat (DegreesStr,DegStr,sizeof (DegreesStr) - 1);
	      }
	   }
     }

   if (!DegreesStr[0])	// Degrees not found
      return soap_sender_fault (soap,
	                        "Bad list of degrees codes",
	                        "Degrees codes must be integers greater than 0 separated by commas");

   /***** Check lowerScore and upperScore *****/
   if (lowerScore < -1.0 || lowerScore > 1.0 ||
       upperScore < -1.0 || upperScore > 1.0 ||
       upperScore < lowerScore)
      return soap_sender_fault (soap,
	                        "Bad score interval",
	                        "lowerScore or upperScore values not valid");

   /***** Get test questions *****/
   if (Qst_DB_GetTrivialQst (&mysql_res,DegreesStr,lowerScore,upperScore))	// Question found
     {
      /* Get question */
      row = mysql_fetch_row (mysql_res);

      /* Get question code (row[0]) */
      QstCod = Str_ConvertStrCodToLongCod (row[0]);
      getTrivialQuestionOut->question.questionCode = (int) QstCod;

      /* Get answer type (row[1]) */
      AnswerType = Qst_ConvertFromStrAnsTypDBToAnsTyp (row[1]);
      getTrivialQuestionOut->question.answerType =
         soap_malloc (soap,Qst_MAX_BYTES_ANSWER_TYPE + 1);
      Str_Copy (getTrivialQuestionOut->question.answerType,
                Qst_StrAnswerTypesXML[AnswerType],Qst_MAX_BYTES_ANSWER_TYPE);

      /* Get shuffle (row[2]) */
      getTrivialQuestionOut->question.shuffle = row[2][0] == 'Y' ? 1 :
							           0;

      /* Get question stem (row[3]) */
      getTrivialQuestionOut->question.stem =
         soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
      Str_Copy (getTrivialQuestionOut->question.stem,row[3],Cns_MAX_BYTES_TEXT);

      /* Get question feedback (row[4]) */
      getTrivialQuestionOut->question.feedback =
         soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
      Str_Copy (getTrivialQuestionOut->question.feedback,row[4],Cns_MAX_BYTES_TEXT);
     }
   else		// Empty question
     {
      /* Question code (row[0]) */
      QstCod = -1L;
      getTrivialQuestionOut->question.questionCode = -1;

      /* Answer type (row[1]) */
      getTrivialQuestionOut->question.answerType = soap_malloc (soap,1);
      getTrivialQuestionOut->question.answerType[0] = '\0';

      /* Shuffle (row[2]) */
      getTrivialQuestionOut->question.shuffle = 0;

      /* Question stem (row[3]) */
      getTrivialQuestionOut->question.stem = soap_malloc (soap,1);
      getTrivialQuestionOut->question.stem[0] = '\0';

      /* Get question feedback (row[4]) */
      getTrivialQuestionOut->question.feedback = soap_malloc (soap,1);
      getTrivialQuestionOut->question.feedback[0] = '\0';
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   if (QstCod > 0)
     {
      /***** Get answer from database *****/
      NumAnss = Qst_DB_GetAnswersData (&mysql_res,QstCod,Qst_DONT_SHUFFLE);

      getTrivialQuestionOut->answersArray.__size = (int) NumAnss;

      if (NumAnss == 0)
	 getTrivialQuestionOut->answersArray.__ptr = NULL;
      else	// Answers found
	{
	 getTrivialQuestionOut->answersArray.__ptr = soap_malloc (soap,
								  (getTrivialQuestionOut->answersArray.__size) *
								  sizeof (*(getTrivialQuestionOut->answersArray.__ptr)));

	 for (NumAns = 0;
	      NumAns < NumAnss;
	      NumAns++)
	   {
	    /* Get next question */
	    row = mysql_fetch_row (mysql_res);

	    /* Get question code */
	    getTrivialQuestionOut->answersArray.__ptr[NumAns].questionCode = (int) QstCod;

	    /* Get answer index (row[0]) */
	    if (sscanf (row[0],"%u",&Index) == 1)
	       getTrivialQuestionOut->answersArray.__ptr[NumAns].answerIndex = (int) Index;
	    else
	       getTrivialQuestionOut->answersArray.__ptr[NumAns].answerIndex = 0;	// error

	    /* Get answer (row[1]) */
	    getTrivialQuestionOut->answersArray.__ptr[NumAns].answerText =
	       soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
	    Str_Copy (getTrivialQuestionOut->answersArray.__ptr[NumAns].answerText,
	              row[3],Cns_MAX_BYTES_TEXT);

	    /* Get feedback (row[2]) */
	    getTrivialQuestionOut->answersArray.__ptr[NumAns].answerFeedback =
	       soap_malloc (soap,Cns_MAX_BYTES_TEXT + 1);
	    Str_Copy (getTrivialQuestionOut->answersArray.__ptr[NumAns].answerFeedback,
	              row[4],Cns_MAX_BYTES_TEXT);

	    // Media code (row[3]) ignored here

	    /* Get correct (row[4]) */
	    getTrivialQuestionOut->answersArray.__ptr[NumAns].correct = row[4][0] == 'Y' ? 1 :
										           0;
	   }
	}

      /***** Free structure that stores the query result *****/
      DB_FreeMySQLResult (&mysql_res);
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/*********************** Return list of games in a course ********************/
/*****************************************************************************/

int swad__getGames (struct soap *soap,
                    char *wsKey,int courseCode,			// input
                    struct swad__getGamesOutput *getGamesOut)	// output
  {
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumGames;
   int NumGame;
   long GamCod;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   long StartTime;
   long EndTime;
   double DoubleNum;
   size_t Length;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getGames;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am logged as student in the course *****/
   if (Gbl.Usrs.Me.Role.Logged != Rol_STD)
      return soap_receiver_fault (soap,
				  "Request forbidden",
				  "Requester must be a student in the course");

   /***** Query list of games available for me *****/
   NumGames = Gam_DB_GetListAvailableGames (&mysql_res);
   getGamesOut->gamesArray.__size =
   getGamesOut->numGames          = (int) NumGames;

   if (getGamesOut->numGames == 0)
      getGamesOut->gamesArray.__ptr = NULL;
   else	// Games found
     {
      getGamesOut->gamesArray.__ptr = soap_malloc (soap,
						   (getGamesOut->gamesArray.__size) *
						   sizeof (*(getGamesOut->gamesArray.__ptr)));

      for (NumGame = 0;
	   NumGame < getGamesOut->numGames;
	   NumGame++)
	{
	 /* Get next game */
	 row = mysql_fetch_row (mysql_res);

	 /* Get game code (row[0]) */
	 GamCod = Str_ConvertStrCodToLongCod (row[0]);
         getGamesOut->gamesArray.__ptr[NumGame].gameCode = (int) GamCod;

	 /* Get user's code of the user who created the game (row[1]) */
         Gbl.Usrs.Other.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[1]);
	 switch (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,
					       Gbl.Hierarchy.Node[Hie_CRS].HieCod))	// Get some user's data from database
	   {
	    case Exi_EXISTS:
	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname1);
	       getGamesOut->gamesArray.__ptr[NumGame].userSurname1 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].userSurname1,
			 Gbl.Usrs.Other.UsrDat.Surname1,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname2);
	       getGamesOut->gamesArray.__ptr[NumGame].userSurname2 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].userSurname2,
			 Gbl.Usrs.Other.UsrDat.Surname2,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.FrstName);
	       getGamesOut->gamesArray.__ptr[NumGame].userFirstname =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].userFirstname,
			 Gbl.Usrs.Other.UsrDat.FrstName,Length);

	       PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
	       Length = strlen (PhotoURL);
	       getGamesOut->gamesArray.__ptr[NumGame].userPhoto =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].userPhoto,
			 PhotoURL,Length);
	       break;
	    case Exi_DOES_NOT_EXIST:
	    default:
	       getGamesOut->gamesArray.__ptr[NumGame].userSurname1  = NULL;
	       getGamesOut->gamesArray.__ptr[NumGame].userSurname2  = NULL;
	       getGamesOut->gamesArray.__ptr[NumGame].userFirstname = NULL;
	       getGamesOut->gamesArray.__ptr[NumGame].userPhoto     = NULL;
	       break;
	   }

	 /* Get game start time (row[2]) */
         StartTime = 0L;
         if (row[2])
            sscanf (row[2],"%ld",&StartTime);
         getGamesOut->gamesArray.__ptr[NumGame].startTime = StartTime;

	 /* Get game end time (row[3]) */
         EndTime = 0L;
         if (row[3])
            sscanf (row[3],"%ld",&EndTime);
         getGamesOut->gamesArray.__ptr[NumGame].endTime = EndTime;

	 /* Get maximum grade (row[4]) */
	 switch (Str_GetDoubleFromStr (row[4],&DoubleNum))
	   {
	    case Err_SUCCESS:
	       getGamesOut->gamesArray.__ptr[NumGame].maxGrade = (float) DoubleNum;
	       if (getGamesOut->gamesArray.__ptr[NumGame].maxGrade < 0.0)	// Only positive values allowed
		  getGamesOut->gamesArray.__ptr[NumGame].maxGrade = 0.0;
	       break;
	    case Err_ERROR:
	    default:
	       getGamesOut->gamesArray.__ptr[NumGame].maxGrade = 0.0;
	       break;
	   }

	 /* Get visibility (row[5]) */
         getGamesOut->gamesArray.__ptr[NumGame].visibility = TstVis_GetVisibilityFromStr (row[5]);

	 /* Get title of the game (row[6]) */
         Length = strlen (row[6]);
         getGamesOut->gamesArray.__ptr[NumGame].title =
            soap_malloc (soap,Length + 1);
         Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].title,row[6],Length);

	 /* Get Txt (row[7]) */
         Length = strlen (row[7]);
         getGamesOut->gamesArray.__ptr[NumGame].text =
            soap_malloc (soap,Length + 1);
         Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].text,row[7],Length);
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/********************** Return list of matches in a game *********************/
/*****************************************************************************/

int swad__getMatches (struct soap *soap,
                      char *wsKey,int courseCode,int gameCode,		// input
                      struct swad__getMatchesOutput *getMatchesOut)	// output
  {
   int ReturnCode;
   struct Gam_Game Game;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumMatches;
   int NumMatch;
   long MchCod;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t PhotoExists;
   long StartTime;
   long EndTime;
   size_t Length;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getMatches;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Reset game *****/
   Gam_ResetGame (&Game);

   /***** Get game data from database *****/
   Game.GamCod = (long) gameCode;
   if (Game.GamCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad game code",
	                        "Game code must be a integer greater than 0");
   Gam_GetGameDataByCod (&Game);

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am logged as student in the course *****/
   if (Gbl.Usrs.Me.Role.Logged != Rol_STD)
      return soap_receiver_fault (soap,
				  "Request forbidden",
				  "Requester must be a student in the course");

   /***** Query list of matches available for me *****/
   NumMatches = Mch_DB_GetAvailableMatchesInGame (&mysql_res,Game.GamCod);
   getMatchesOut->matchesArray.__size =
   getMatchesOut->numMatches          = (int) NumMatches;

   if (getMatchesOut->numMatches == 0)
      getMatchesOut->matchesArray.__ptr = NULL;
   else	// Matches found
     {
      getMatchesOut->matchesArray.__ptr = soap_malloc (soap,
						       (getMatchesOut->matchesArray.__size) *
						       sizeof (*(getMatchesOut->matchesArray.__ptr)));

      for (NumMatch = 0;
	   NumMatch < getMatchesOut->numMatches;
	   NumMatch++)
	{
	 /* Get next game */
	 row = mysql_fetch_row (mysql_res);

	 /* Get match code (row[0]) */
	 MchCod = Str_ConvertStrCodToLongCod (row[0]);
         getMatchesOut->matchesArray.__ptr[NumMatch].matchCode = (int) MchCod;

	 /* Get user's code of the user who created the game (row[1]) */
         Gbl.Usrs.Other.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[1]);
	 switch (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,
					       Gbl.Hierarchy.Node[Hie_CRS].HieCod))	// Get some user's data from database
	   {
	    case Exi_EXISTS:
	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname1);
	       getMatchesOut->matchesArray.__ptr[NumMatch].userSurname1 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getMatchesOut->matchesArray.__ptr[NumMatch].userSurname1,
			 Gbl.Usrs.Other.UsrDat.Surname1,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.Surname2);
	       getMatchesOut->matchesArray.__ptr[NumMatch].userSurname2 =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getMatchesOut->matchesArray.__ptr[NumMatch].userSurname2,
			 Gbl.Usrs.Other.UsrDat.Surname2,Length);

	       Length = strlen (Gbl.Usrs.Other.UsrDat.FrstName);
	       getMatchesOut->matchesArray.__ptr[NumMatch].userFirstname =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getMatchesOut->matchesArray.__ptr[NumMatch].userFirstname,
			 Gbl.Usrs.Other.UsrDat.FrstName,Length);

	       PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
	       Length = strlen (PhotoURL);
	       getMatchesOut->matchesArray.__ptr[NumMatch].userPhoto =
		  soap_malloc (soap,Length + 1);
	       Str_Copy (getMatchesOut->matchesArray.__ptr[NumMatch].userPhoto,
			 PhotoURL,Length);
	       break;
	    case Exi_DOES_NOT_EXIST:
	    default:
	       getMatchesOut->matchesArray.__ptr[NumMatch].userSurname1  = NULL;
	       getMatchesOut->matchesArray.__ptr[NumMatch].userSurname2  = NULL;
	       getMatchesOut->matchesArray.__ptr[NumMatch].userFirstname = NULL;
	       getMatchesOut->matchesArray.__ptr[NumMatch].userPhoto     = NULL;
	       break;
	   }

	 /* Get match start time (row[2]) */
         StartTime = 0L;
         if (row[2])
            sscanf (row[2],"%ld",&StartTime);
         getMatchesOut->matchesArray.__ptr[NumMatch].startTime = StartTime;

	 /* Get match end time (row[3]) */
         EndTime = 0L;
         if (row[3])
            sscanf (row[3],"%ld",&EndTime);
         getMatchesOut->matchesArray.__ptr[NumMatch].endTime = EndTime;

	 /* Get title of the match (row[4]) */
         Length = strlen (row[4]);
         getMatchesOut->matchesArray.__ptr[NumMatch].title =
            soap_malloc (soap,Length + 1);
         Str_Copy (getMatchesOut->matchesArray.__ptr[NumMatch].title,row[4],
                   Length);

	 /* Get current question index (row[5]) */
         getMatchesOut->matchesArray.__ptr[NumMatch].questionIndex = (int) Str_ConvertStrToUnsigned (row[5]);

	 /* Get list of groups for this match */
	 API_GetListGrpsInMatchFromDB (soap,
				       MchCod,&(getMatchesOut->matchesArray.__ptr[NumMatch].groups));
	}
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return SOAP_OK;
  }

/*****************************************************************************/
/*********** Get match status to be refreshed in student's screen ************/
/*****************************************************************************/

int swad__getMatchStatus (struct soap *soap,
                          char *wsKey,int courseCode,int gameCode,int matchCode,	// input
                          struct swad__getMatchStatusOutput *getMatchStatusOut)		// output
  {
   int ReturnCode;
   struct Gam_Game Game;
   struct Mch_Match Match;
   struct Mch_UsrAnswer UsrAnswer;

   /***** Reset game and match *****/
   Gam_ResetGame (&Game);
   Mch_ResetMatch (&Match);

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getMatchStatus;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;
   Game.GamCod = (long) gameCode;
   Match.MchCod = (long) matchCode;

   /***** Default output *****/
   getMatchStatusOut->matchCode = -1;
   getMatchStatusOut->questionIndex = -1;
   getMatchStatusOut->numAnswers = -1;
   getMatchStatusOut->answerIndex = -1;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get game data from database *****/
   if (Game.GamCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad game code",
	                        "Game code must be a integer greater than 0");
   Gam_GetGameDataByCod (&Game);

   /***** Get match data from database *****/
   if (Match.MchCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad match code",
	                        "Match code must be a integer greater than 0");
   Mch_GetMatchDataByCod (&Match);

   /* Check that match belongs to game */
   if (Match.GamCod != Game.GamCod)
      return soap_sender_fault (soap,
	                        "Bad game code",
	                        "Match does not belong to game");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am logged as student in the course *****/
   if (Gbl.Usrs.Me.Role.Logged != Rol_STD)
      return soap_receiver_fault (soap,
				  "Request forbidden",
				  "Requester must be a student in the course");

   /***** Can I play this match? *****/
   if (Mch_CheckIfICanPlayThisMatchBasedOnGrps (&Match) == Usr_CAN_NOT)
      return soap_receiver_fault (soap,
				  "Request forbidden",
				  "Requester can not join this match");

   /***** Set default match code *****/
   getMatchStatusOut->matchCode = 0;	// == 0 ==> wait

   /***** Set index of question inside the game *****/
   getMatchStatusOut->questionIndex = (int) Match.Status.QstInd;

   if (Match.Status.Playing &&		// Match is being played
       Match.Status.Showing != Mch_END)	// Unfinished
      /***** Join match *****/
      /* Update players */
      if (Mch_RegisterMeAsPlayerInMatch (&Match) == Err_SUCCESS)
	 if (Match.Status.Showing == Mch_ANSWERS)	// Showing the question stem and the answers
	    getMatchStatusOut->matchCode = (int) Match.MchCod;	// > 0 ==> student is allowed to answer the question

   /***** Set number of answers (options) in question and student's answer (<0 ==> no answer) *****/
   switch (Match.Status.Showing)
     {
      case Mch_START:	// Start: don't show anything
      case Mch_END:	// End: don't show anything
         getMatchStatusOut->numAnswers = 0;
         getMatchStatusOut->answerIndex = -1;
         break;
      case Mch_STEM:	// Showing only the question stem
      case Mch_ANSWERS:	// Showing the question stem and the answers
      case Mch_RESULTS:	// Showing the results
	 getMatchStatusOut->numAnswers = (int) Qst_DB_GetNumAnswersQst (Match.Status.QstCod);
	 Mch_GetQstAnsFromDB (Match.MchCod,
			      Gbl.Usrs.Me.UsrDat.UsrCod,
			      Match.Status.QstInd,
			      &UsrAnswer);
	 getMatchStatusOut->answerIndex = UsrAnswer.NumOpt;
         break;
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/************* Send an answer to the current question in a match *************/
/*****************************************************************************/

int swad__answerMatchQuestion (struct soap *soap,
                               char *wsKey,int courseCode,int gameCode,int matchCode,int questionIndex,int numOption,	// input
                               struct swad__answerMatchQuestionOutput *answerMatchQuestionOut)				// output
  {
   int ReturnCode;
   struct Gam_Game Game;
   struct Mch_Match Match;
   unsigned QstInd;	// 0 means that the game has not started. First question has index 1.
   struct Mch_UsrAnswer UsrAnswer;

   /***** Reset game and match *****/
   Gam_ResetGame (&Game);
   Mch_ResetMatch (&Match);

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_answerMatchQuestion;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = (long) courseCode;
   Game.GamCod  = (long) gameCode;
   Match.MchCod = (long) matchCode;

   /***** Set default output *****/
   /*	<= 0 on error
	 > 0 on sucess */
   answerMatchQuestionOut->matchCode = -1;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Node[Hie_CRS].HieCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Get game data from database *****/
   if (Game.GamCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad game code",
	                        "Game code must be a integer greater than 0");
   Gam_GetGameDataByCod (&Game);

   /***** Get match data from database *****/
   if (Match.MchCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad match code",
	                        "Match code must be a integer greater than 0");
   Mch_GetMatchDataByCod (&Match);

   /***** Check that match belongs to game *****/
   if (Match.GamCod != Game.GamCod)
      return soap_sender_fault (soap,
	                        "Bad game code",
	                        "Match does not belong to game");

   /***** Check question index *****/
   if (questionIndex <= 0)
      return soap_sender_fault (soap,
	                        "Bad question index",
	                        "The question index should be greater than 0");
   QstInd = (unsigned) questionIndex;

   /***** Check number of option selected by student *****/
   if (numOption < 0)
      /***** Remove my answer to this question *****/
      Mch_RemoveMyQuestionAnswer (&Match,QstInd);
   else
     {
      /***** Store answer *****/
      UsrAnswer.NumOpt = (unsigned) numOption;
      Mch_StoreQuestionAnswer (&Match,QstInd,&UsrAnswer);
     }

   /***** Set success output *****/
   answerMatchQuestionOut->matchCode = Match.MchCod;

   return SOAP_OK;
  }

/*****************************************************************************/
/*********************** Get lists of groups of a match **********************/
/*****************************************************************************/

static void API_GetListGrpsInMatchFromDB (struct soap *soap,
					  long MchCod,char **ListGroups)
  {
   MYSQL_RES *mysql_res;
   long NumGrps;
   long NumGrp;
   long GrpCod;
   char GrpCodStr[Cns_MAX_DIGITS_LONG + 1];
   size_t Length;

   /***** Get list of groups *****/
   if ((NumGrps = Mch_DB_GetGrpCodsAssociatedToMatch (&mysql_res,MchCod)))	// Groups found
     {
      Length = NumGrps * (Cns_MAX_DIGITS_LONG + 1) - 1;
      *ListGroups = soap_malloc (soap,Length + 1);
      (*ListGroups)[0] = '\0';

      for (NumGrp = 0;
	   NumGrp < NumGrps;
	   NumGrp++)
	{
	 /* Get next group */
	 GrpCod = DB_GetNextCode (mysql_res);
	 snprintf (GrpCodStr,sizeof (GrpCodStr),NumGrp ? ",%ld" :
			                                 "%ld",
		   GrpCod);
	 Str_Concat (*ListGroups,GrpCodStr,Length);
	}
     }
   else
      *ListGroups = NULL;

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/*************** Get a directory tree in a course or a group *****************/
/*****************************************************************************/
// TODO: should treeCode be a string with one of the followins values?
// documents
// shared
// marks

int swad__getDirectoryTree (struct soap *soap,
                            char *wsKey,int courseCode,int groupCode,int treeCode,	// input
                            struct swad__getDirectoryTreeOutput *getDirectoryTreeOut)	// output
  {
   extern const char *Brw_RootFolderInternalNames[Brw_NUM_TYPES_FILE_BROWSER];
   int ReturnCode;
   char XMLFileName[PATH_MAX + 1];
   FILE *XML;
   unsigned long FileSize;
   unsigned long NumBytesRead;
   long GrpCod;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getDirectoryTree;
   Gbl.Hierarchy.Node[Hie_CRS].HieCod = courseCode > 0 ? (long) courseCode :
	                                                 -1L;
   GrpCod = groupCode > 0 ? (long) groupCode :
	                    -1L;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (soap,
						   Gbl.Hierarchy.Node[Hie_CRS].HieCod,
						   GrpCod)) != SOAP_OK)
      return ReturnCode;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Check if I belong to course/group *****/
   if (GrpCod > 0)
      if (Grp_GetIfIBelongToGrp (GrpCod) == Usr_DONT_BELONG)
	 return soap_receiver_fault (soap,
				     "Request forbidden",
				     "Requester must belong to group");

   /***** Check if tree code is correct *****/
   if (treeCode <= 0 || treeCode > 3)
      return soap_sender_fault (soap,
	                        "Bad tree code",
	                        "Tree code must be 1 (documents), 2 (shared files) or 3 (marks)");

   /***** Return directory tree *****/
   /* Get directory tree into XML file */
   if (courseCode > 0)
     {
      if (groupCode > 0)
	 switch (treeCode)
	   {
	    case 1:	// Documents
	       Gbl.FileBrowser.Type = Brw_SHOW_DOC_GRP;
	       break;
	    case 2:	// Shared files
	       Gbl.FileBrowser.Type = Brw_ADMI_SHR_GRP;
	       break;
	    case 3:	// Marks
	       Gbl.FileBrowser.Type = Brw_SHOW_MRK_GRP;
	       break;
	   }
      else	// groupCode <= 0
	 switch (treeCode)
	   {
	    case 1:	// Documents
	       Gbl.FileBrowser.Type = Brw_SHOW_DOC_CRS;
	       break;
	    case 2:	// Shared files
	       Gbl.FileBrowser.Type = Brw_ADMI_SHR_CRS;
	       break;
	    case 3:	// Marks
	       Gbl.FileBrowser.Type = Brw_SHOW_MRK_CRS;
	       break;
	   }
     }
   else
      return soap_sender_fault (soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /* Initialize path to private directory */
   snprintf (Gbl.Crs.Path.AbsPriv,sizeof (Gbl.Crs.Path.AbsPriv),"%s/%ld",
             Cfg_PATH_CRS_PRIVATE,Gbl.Hierarchy.Node[Hie_CRS].HieCod);
   Brw_SetGrpCod (GrpCod);
   Brw_InitializeFileBrowser ();
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Path,Brw_RootFolderInternalNames[Gbl.FileBrowser.Type],
	     sizeof (Gbl.FileBrowser.FilFolLnk.Path) - 1);
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Name,".",
	     sizeof (Gbl.FileBrowser.FilFolLnk.Name) - 1);
   Brw_SetFullPathInTree ();

   /* Check if exists the directory for HTML output. If not exists, create it */
   Fil_CreateDirIfNotExists (Cfg_PATH_OUT_PRIVATE);

   /* Create a unique name for the file */
   snprintf (XMLFileName,sizeof (XMLFileName),"%s/%s.xml",
             Cfg_PATH_OUT_PRIVATE,Cry_GetUniqueNameEncrypted ());

   /* Open file for writing and reading */
   if ((XML = fopen (XMLFileName,"w+t")) == NULL)
      return soap_receiver_fault (soap,
	                          "Can not get tree",
	                          "Can not create temporary XML file");

   /* Get directory tree into XML file */
   XML_WriteStartFile (XML,"tree");
   if (Brw_CheckIfFileOrFolderIsHidden (Brw_IS_FOLDER,
                                        Gbl.FileBrowser.FilFolLnk.Full) == HidVis_VISIBLE)
      API_ListDir (XML,1,
                   Gbl.FileBrowser.Path.RootFolder,
                   Brw_RootFolderInternalNames[Gbl.FileBrowser.Type]);
   XML_WriteEndFile (XML,"tree");

   /* Compute file size */
   // fseek (XML,0L,SEEK_END);
   FileSize = (unsigned long) ftell (XML);
   fseek (XML,0L,SEEK_SET);

   /* Copy XML content from file to memory */
   getDirectoryTreeOut->tree = soap_malloc (soap,FileSize + 1);
   NumBytesRead = fread (getDirectoryTreeOut->tree,1,FileSize,XML);
   getDirectoryTreeOut->tree[NumBytesRead] = '\0';

   /* Close and remove XML file */
   fclose (XML);
   unlink (XMLFileName);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************ List a directory recursively ***********************/
/*****************************************************************************/

static void API_ListDir (FILE *XML,unsigned Level,
                         const char *Path,const char *PathInTree)
  {
   extern const char *Txt_NEW_LINE;
   struct dirent **FileList;
   int NumFile;
   int NumFiles;
   char PathFileRel[PATH_MAX + 1 + NAME_MAX + 1];
   char PathFileInExplTree[PATH_MAX + 1 + NAME_MAX + 1];
   struct stat FileStatus;
   __attribute__((unused)) HidVis_HiddenOrVisible_t HiddenOrVisible;

   /***** Scan directory *****/
   if ((NumFiles = scandir (Path,&FileList,NULL,alphasort)) >= 0)	// No error
     {
      /***** List files *****/
      for (NumFile = 0;
	   NumFile < NumFiles;
	   NumFile++)
	{
	 if (strcmp (FileList[NumFile]->d_name,".") &&
	     strcmp (FileList[NumFile]->d_name,".."))	// Skip directories "." and ".."
	   {
	    snprintf (PathFileRel,sizeof (PathFileRel),"%s/%s",
		      Path,FileList[NumFile]->d_name);
	    snprintf (PathFileInExplTree,sizeof (PathFileInExplTree),"%s/%s",
		      PathInTree,FileList[NumFile]->d_name);

	    Str_Copy (Gbl.FileBrowser.FilFolLnk.Path,PathInTree,
	 	      sizeof (Gbl.FileBrowser.FilFolLnk.Path) - 1);
	    Str_Copy (Gbl.FileBrowser.FilFolLnk.Name,FileList[NumFile]->d_name,
	 	      sizeof (Gbl.FileBrowser.FilFolLnk.Name) - 1);

	    if (!lstat (PathFileRel,&FileStatus))	// On success ==> 0 is returned
	      {
	       /***** Construct the full path of the file or folder *****/
	       Brw_SetFullPathInTree ();

	       if (S_ISDIR (FileStatus.st_mode))		// It's a directory
		 {
		  /***** Write a row for the subdirectory *****/
		  if (API_WriteRowFileBrowser (XML,Level,
		                               Brw_IS_FOLDER,
		                               FileList[NumFile]->d_name) == HidVis_VISIBLE)
		    {
		     /* List subtree starting at this this directory */
		     API_ListDir (XML,Level + 1,
		                  PathFileRel,PathFileInExplTree);

		     /* Indent and end dir */
		     API_IndentXMLLine (XML,Level);
		     fprintf (XML,"</dir>%s",Txt_NEW_LINE);
		    }
		 }
	       else if (S_ISREG (FileStatus.st_mode))	// It's a regular file
		  HiddenOrVisible = API_WriteRowFileBrowser (XML,Level,
							     Str_FileIs (FileList[NumFile]->d_name,"url") ? Brw_IS_LINK :
													    Brw_IS_FILE,
							     FileList[NumFile]->d_name);
	      }
	   }

	 free (FileList[NumFile]);
	}
      free (FileList);
     }
  }

/*****************************************************************************/
/*********************** Write a row of a file browser ***********************/
/*****************************************************************************/
// If it is the first row (root folder), always show it
// If it is not the first row, it is shown or not depending on whether it is hidden or not
// Return if the row is visible or hidden

static HidVis_HiddenOrVisible_t API_WriteRowFileBrowser (FILE *XML,unsigned Level,
							 Brw_FileType_t FileType,
							 const char *FileName)
  {
   extern const char *Txt_NEW_LINE;
   extern const char *Txt_LICENSES[Brw_NUM_LICENSES];
   struct Brw_FileMetadata FileMetadata;
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t FileExists;
   __attribute__((unused)) Exi_Exist_t UsrExists;
   __attribute__((unused)) Exi_Exist_t PhotoExists;

   /***** Is this row hidden or visible? *****/
   if (Gbl.FileBrowser.Type == Brw_SHOW_DOC_CRS ||
       Gbl.FileBrowser.Type == Brw_SHOW_DOC_GRP)
      if (Brw_CheckIfFileOrFolderIsHidden (FileType,
                                           Gbl.FileBrowser.FilFolLnk.Full) == HidVis_HIDDEN)
	 return HidVis_HIDDEN;

   /***** XML row *****/
   /* Indent */
   API_IndentXMLLine (XML,Level);

   /* Write file or folder data */
   if (FileType == Brw_IS_FOLDER)
      fprintf (XML,"<dir name=\"%s\">%s",FileName,Txt_NEW_LINE);
   else	// File or link
     {
      /* Get file metadata */
      Brw_GetFileMetadataByPath (&FileMetadata);
      FileExists = Brw_GetFileTypeSizeAndDate (&FileMetadata);

      if (FileMetadata.FilCod <= 0)	// No entry for this file in database table of files
	 /* Add entry to the table of files/folders */
	 FileMetadata.FilCod = Brw_DB_AddPath (-1L,FileMetadata.FilFolLnk.Type,
	                                        Gbl.FileBrowser.FilFolLnk.Full,
	                                        PriPub_PRIVATE,Brw_LICENSE_DEFAULT);

      Gbl.Usrs.Other.UsrDat.UsrCod = FileMetadata.PublisherUsrCod;
      UsrExists = Usr_ChkUsrCodAndGetAllUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,
							   Usr_DONT_GET_PREFS,
							   Usr_DONT_GET_ROLE_IN_CRS);
      PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,PhotoURL);

      fprintf (XML,"<file name=\"%s\">"
		   "<code>%ld</code>"
		   "<size>%lu</size>"
		   "<time>%lu</time>"
		   "<license>%s</license>"
		   "<publisher>%s</publisher>"
		   "<photo>%s</photo>"
		   "</file>%s",
	       FileName,
	       FileMetadata.FilCod,
	       (unsigned long) FileMetadata.Size,
	       (unsigned long) FileMetadata.Time,
	       Txt_LICENSES[FileMetadata.License],
	       Gbl.Usrs.Other.UsrDat.FullName,
	       PhotoURL,
	       Txt_NEW_LINE);
     }

   return HidVis_VISIBLE;
  }

/*****************************************************************************/
/******************************* Indent XML line *****************************/
/*****************************************************************************/

static void API_IndentXMLLine (FILE *XML,unsigned Level)
  {
   for ( ;
	Level;
	Level--)
      fprintf (XML,"\t");
  }

/*****************************************************************************/
/**************************** Get info of a file *****************************/
/*****************************************************************************/

int swad__getFile (struct soap *soap,
                   char *wsKey,int fileCode,			// input
                   struct swad__getFileOutput *getFileOut)	// output
  {
   extern const char *Txt_LICENSES[Brw_NUM_LICENSES];
   int ReturnCode;
   struct Brw_FileMetadata FileMetadata;
   long GrpCod;
   char URL[WWW_MAX_BYTES_WWW + 1];
   char PhotoURL[WWW_MAX_BYTES_WWW + 1];
   __attribute__((unused)) Exi_Exist_t FileExists;
   __attribute__((unused)) Exi_Exist_t PhotoExists;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getFile;

   /***** Allocate space for strings *****/
   getFileOut->fileName       = soap_malloc (soap,NAME_MAX                + 1);
   getFileOut->URL            = soap_malloc (soap,WWW_MAX_BYTES_WWW       + 1);
   getFileOut->license        = soap_malloc (soap,Brw_MAX_BYTES_LICENSE   + 1);
   getFileOut->publisherName  = soap_malloc (soap,Usr_MAX_BYTES_FULL_NAME + 1);
   getFileOut->publisherPhoto = soap_malloc (soap,WWW_MAX_BYTES_WWW       + 1);

   /***** Default values returned on error *****/
   getFileOut->fileName[0]       = '\0';
   getFileOut->URL[0]            = '\0';
   getFileOut->size	         = 0;
   getFileOut->time	         = 0;
   getFileOut->license[0]        = '\0';
   getFileOut->publisherName[0]  = '\0';
   getFileOut->publisherPhoto[0] = '\0';

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get file metadata *****/
   FileMetadata.FilCod = (long) fileCode;
   if (FileMetadata.FilCod <= 0)
      return soap_sender_fault (soap,
	                        "Bad file code",
	                        "File code must be a integer greater than 0");

   Brw_GetFileMetadataByCod (&FileMetadata);
   if (FileMetadata.FilCod <= 0)
      return soap_sender_fault (soap,
	                        "File does not exist, please refresh",
	                        "The file requested does not exists");

   /***** Set course and group codes *****/
   Brw_GetCrsGrpFromFileMetadata (FileMetadata.FileBrowser,FileMetadata.Cod,
                                  &Gbl.Hierarchy.Node[Hie_INS].HieCod,
                                  &Gbl.Hierarchy.Node[Hie_CTR].HieCod,
                                  &Gbl.Hierarchy.Node[Hie_DEG].HieCod,
                                  &Gbl.Hierarchy.Node[Hie_CRS].HieCod,
                                  &GrpCod);
   Hie_InitHierarchy ();

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (soap,
						   Gbl.Hierarchy.Node[Hie_CRS].HieCod,
						   GrpCod)) != SOAP_OK)
      return ReturnCode;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Check if I belong to group *****/
   if (GrpCod > 0)
      if (Grp_GetIfIBelongToGrp (GrpCod) == Usr_DONT_BELONG)
	 return soap_receiver_fault (soap,
				     "Request forbidden",
				     "Requester must belong to group");

   /***** Check if file is in a valid zone *****/
   Gbl.FileBrowser.Type = FileMetadata.FileBrowser;
   switch (Gbl.FileBrowser.Type)
     {
      case Brw_ADMI_DOC_CRS:
      case Brw_ADMI_DOC_GRP:
      case Brw_ADMI_SHR_CRS:
      case Brw_ADMI_SHR_GRP:
         break;
      case Brw_ADMI_MRK_CRS:
      case Brw_ADMI_MRK_GRP:
	 // Downloading a file of marks is only allowed for teachers
	 if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
	    return soap_receiver_fault (soap,
					"Wrong tree",
					"Wrong file zone");
	 break;
      default:
         return soap_sender_fault (soap,
                                   "Wrong tree",
                                   "Wrong file zone");
     }

   /***** Set paths *****/
   Brw_SetGrpCod (GrpCod);
   Brw_InitializeFileBrowser ();
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Path,FileMetadata.FilFolLnk.Path,
	     sizeof (Gbl.FileBrowser.FilFolLnk.Path) - 1);
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Name,FileMetadata.FilFolLnk.Name,
	     sizeof (Gbl.FileBrowser.FilFolLnk.Name) - 1);
   Brw_SetFullPathInTree ();

   /***** Get file size and date *****/
   FileExists = Brw_GetFileTypeSizeAndDate (&FileMetadata);

   /***** Update number of views *****/
   Brw_GetAndUpdateFileViews (&FileMetadata);

   /***** Create and get link to download the file *****/
   Brw_GetLinkToDownloadFile (FileMetadata.FilFolLnk.Path,
                              FileMetadata.FilFolLnk.Name,
	                      URL);

   /***** Copy data into output structure *****/
   Str_Copy (getFileOut->fileName,FileMetadata.FilFolLnk.Name,NAME_MAX);
   Str_Copy (getFileOut->URL,URL,WWW_MAX_BYTES_WWW);
   getFileOut->size = (int) FileMetadata.Size;
   getFileOut->time = (int) FileMetadata.Time;
   Str_Copy (getFileOut->license,Txt_LICENSES[FileMetadata.License],
             Brw_MAX_BYTES_LICENSE);

   if ((Gbl.Usrs.Other.UsrDat.UsrCod = FileMetadata.PublisherUsrCod) > 0)
      /* Get publisher's data */
      if (Usr_ChkUsrCodAndGetAllUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,
                                                   Usr_DONT_GET_PREFS,
                                                   Usr_DONT_GET_ROLE_IN_CRS) == Exi_EXISTS)
	{
	 /* Copy publisher's data into output structure */
	 Str_Copy (getFileOut->publisherName,Gbl.Usrs.Other.UsrDat.FullName,
	           Usr_MAX_BYTES_FULL_NAME);

	 PhotoExists = Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
	 Str_Copy (getFileOut->publisherPhoto,PhotoURL,WWW_MAX_BYTES_WWW);
	}

   return SOAP_OK;
  }

/*****************************************************************************/
/**************************** Get info of my marks ***************************/
/*****************************************************************************/

int swad__getMarks (struct soap *soap,
                    char *wsKey,int fileCode,			// input
                    struct swad__getMarksOutput *getMarksOut)	// output
  {
   int ReturnCode;
   struct Brw_FileMetadata FileMetadata;
   long GrpCod;
   char SummaryStr[Ntf_MAX_BYTES_SUMMARY + 1];	// Not used
   char *ContentStr;
   size_t Length;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getMarks;
   getMarksOut->content = NULL;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get file metadata *****/
   FileMetadata.FilCod = (long) fileCode;
   Brw_GetFileMetadataByCod (&FileMetadata);

   if (FileMetadata.FilFolLnk.Type != Brw_IS_FILE ||
       FileMetadata.Hidden == HidVis_HIDDEN ||
       (FileMetadata.FileBrowser != Brw_ADMI_MRK_CRS &&
	FileMetadata.FileBrowser != Brw_ADMI_MRK_GRP))
      return soap_receiver_fault (soap,
	                          "Bad file code",
	                          "You can not get marks from this file");

   /***** Set course and group codes *****/
   Brw_GetCrsGrpFromFileMetadata (FileMetadata.FileBrowser,FileMetadata.Cod,
                                  &Gbl.Hierarchy.Node[Hie_INS].HieCod,
                                  &Gbl.Hierarchy.Node[Hie_CTR].HieCod,
                                  &Gbl.Hierarchy.Node[Hie_DEG].HieCod,
                                  &Gbl.Hierarchy.Node[Hie_CRS].HieCod,
                                  &GrpCod);

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (soap,
						   Gbl.Hierarchy.Node[Hie_CRS].HieCod,
						   GrpCod)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs != Rol_TCH)
      return soap_receiver_fault (soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Check if I belong to group *****/
   if (GrpCod > 0)
      if (Grp_GetIfIBelongToGrp (GrpCod) == Usr_DONT_BELONG)
	 return soap_receiver_fault (soap,
				     "Request forbidden",
				     "Requester must belong to group");

   /***** Get content *****/
   ContentStr = NULL;
   Mrk_GetNotifMyMarks (SummaryStr,	// Not used
                        &ContentStr,
                        FileMetadata.FilCod,Gbl.Usrs.Me.UsrDat.UsrCod,
                        Ntf_GET_CONTENT);
   if (ContentStr != NULL)
     {
      Length = strlen (ContentStr);
      getMarksOut->content = soap_malloc (soap,Length + 1);
      Str_Copy (getMarksOut->content,ContentStr,Length);
      free (ContentStr);
      ContentStr = NULL;
     }
   else
     {
      getMarksOut->content = soap_malloc (soap,1);
      getMarksOut->content[0] = '\0';
     }

   return SOAP_OK;
  }

/*****************************************************************************/
/**************** Get locations associated to a MAC address ******************/
/*****************************************************************************/

int swad__getLocation (struct soap *soap,
                       char *wsKey,char *MAC,				// input
                       struct swad__getLocationOutput *getLocationOut)	// output
  {
   int ReturnCode;
   unsigned long long MACnum;
   MYSQL_RES *mysql_res;
   Exi_Exist_t LocationExists;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getLocation;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Convert MAC string to number *****/
   if (sscanf (MAC,"%llx",&MACnum) != 1)
      return soap_receiver_fault (soap,
	                          "Bad MAC",
	                          "MAC address format should be 12 hexadecimal digits");

   /***** Get location *****/
   LocationExists = Roo_DB_GetLocationByMAC (&mysql_res,MACnum);

   API_GetLocationData (soap,
                        &(getLocationOut->location),
                        NULL,	// Don't get check in time
                        &mysql_res,LocationExists);

   return SOAP_OK;
  }

/*****************************************************************************/
/***************** Check in (send user's current location) *******************/
/*****************************************************************************/

int swad__sendMyLocation (struct soap *soap,
                          char *wsKey,int roomCode,				// input
                          struct swad__sendMyLocationOutput *sendMyLocationOut)	// output
  {
   int ReturnCode;
   long ChkCod;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_sendMyLocation;

   /***** Default values returned on error *****/
   sendMyLocationOut->success = 0;	// error

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Get some of my data *****/
   if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,
				     Gbl.Hierarchy.Node[Hie_CRS].HieCod) == Exi_DOES_NOT_EXIST)
      return soap_receiver_fault (soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs;

   /***** Check in (insert pair user-room) in the database *****/
   /* Get the code of the inserted item */
   ChkCod = Roo_DB_CheckIn (roomCode);

   /***** Return notification code *****/
   sendMyLocationOut->success = ChkCod > 0 ? 1 :
					     0;

   return SOAP_OK;
  }

/*****************************************************************************/
/***************** Check in (send user's current location) *******************/
/*****************************************************************************/

int swad__getLastLocation (struct soap *soap,
                           char *wsKey,int userCode,					// input
                           struct swad__getLastLocationOutput *getLastLocationOut)	// output
  {
   int ReturnCode;
   MYSQL_RES *mysql_res;
   Exi_Exist_t LocationExists;

   /***** Initializations *****/
   API_Set_gSOAP_RuntimeEnv (soap);
   Gbl.WebService.Function = API_getLastLocation;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckAPIKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if I can see user's location *****/
   /*
   I can only consult the location of another user
   if the intersection of the centers of our courses is not empty.
   The other user does not have to share any course with me,
   but at least some course of each one has to share center.
   */
   switch (Roo_DB_CheckIfICanSeeUsrLocation ((long) userCode))
     {
      case Usr_CAN:
	 /***** Get list of locations *****/
	 LocationExists = Roo_DB_GetUsrLastLocation (&mysql_res,(long) userCode);
	 API_GetLocationData (soap,
			      &(getLastLocationOut->location),
			      &(getLastLocationOut->checkinTime),	// Get check in time
			      &mysql_res,LocationExists);
	 break;
      case Usr_CAN_NOT:
      default:
	 /* I can not see user's location ==> reset output */
	 API_ResetLocation (soap, &(getLastLocationOut->location));
	 getLastLocationOut->checkinTime = 0L;
	 break;
    }

   return SOAP_OK;
  }

/*****************************************************************************/
/************************* Get assignment data *******************************/
/*****************************************************************************/

static void API_GetLocationData (struct soap *soap,
                                 struct swad__location *location,
                                 time_t *CheckInTime,
                                 MYSQL_RES **mysql_res,
				 Exi_Exist_t LocationExists)
  {
   MYSQL_ROW row;
   size_t Length;

   /***** Get data of location from database *****/
   switch (LocationExists)
     {
      case Exi_EXISTS:
	 /* Get row */
	 row = mysql_fetch_row (*mysql_res);
	 /*
	 ins_instits.InsCod				// row[ 0]
	 ins_instits.ShortName				// row[ 1]
	 ins_instits.FullName				// row[ 2]
	 ctr_centers.CtrCod				// row[ 3]
	 ctr_centers.ShortName				// row[ 4]
	 ctr_centers.FullName				// row[ 5]
	 bld_buildings.BldCod				// row[ 6]
	 bld_buildings.ShortName			// row[ 7]
	 bld_buildings.FullName				// row[ 8]
	 roo_rooms.Floor				// row[ 9]
	 roo_rooms.RooCod				// row[10]
	 roo_rooms.ShortName				// row[11]
	 roo_rooms.FullName				// row[12]
	 UNIX_TIMESTAMP(roo_check_in.CheckInTime)	// row[13] (optional)
	 */

	 /* Get institution code (row[0]) */
	 location->institutionCode = (int) Str_ConvertStrCodToLongCod (row[0]);

	 /* Get institution short name (row[1]) */
	 Length = strlen (row[1]);
	 location->institutionShortName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->institutionShortName,row[1],Length);

	 /* Get institution full name (row[2]) */
	 Length = strlen (row[2]);
	 location->institutionFullName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->institutionFullName,row[2],Length);

	 /* Get center code (row[3]) */
	 location->centerCode = (int) Str_ConvertStrCodToLongCod (row[3]);

	 /* Get center short name (row[4]) */
	 Length = strlen (row[4]);
	 location->centerShortName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->centerShortName,row[4],Length);

	 /* Get center full name (row[5]) */
	 Length = strlen (row[5]);
	 location->centerFullName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->centerFullName,row[5],Length);

	 /* Get building code (row[6]) */
	 location->buildingCode = (int) Str_ConvertStrCodToLongCod (row[6]);

	 /* Get building short name (row[7]) */
	 Length = strlen (row[7]);
	 location->buildingShortName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->buildingShortName,row[7],Length);

	 /* Get building full name (row[8]) */
	 Length = strlen (row[8]);
	 location->buildingFullName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->buildingFullName,row[8],Length);

	 /* Get floor (row[9]) */
	 location->floor = (int) Str_ConvertStrCodToLongCod (row[9]);

	 /* Get room code (row[10]) */
	 location->roomCode = (int) Str_ConvertStrCodToLongCod (row[10]);

	 /* Get room short name (row[11]) */
	 Length = strlen (row[11]);
	 location->roomShortName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->roomShortName,row[11],Length);

	 /* Get room full name (row[12]) */
	 Length = strlen (row[12]);
	 location->roomFullName = soap_malloc (soap,Length + 1);
	 Str_Copy (location->roomFullName,row[12],Length);

	 /* Get check in time (row[13]) */
	 if (CheckInTime)
	   {
	    *CheckInTime = 0L;
	    if (row[13])
	       sscanf (row[13],"%ld",CheckInTime);
	   }
	 break;
      case Exi_DOES_NOT_EXIST:
      default:
	 /* No room found ==> reset output */
	 API_ResetLocation (soap,location);

	 if (CheckInTime)
	    *CheckInTime = 0L;
	 break;
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (mysql_res);
  }

/*****************************************************************************/
/************************* Get assignment data *******************************/
/*****************************************************************************/

static void API_ResetLocation (struct soap *soap,
                               struct swad__location *location)
  {
   location->institutionCode         = -1;
   location->institutionShortName    = soap_malloc (soap,1);
   location->institutionShortName[0] = '\0';
   location->institutionFullName     = soap_malloc (soap,1);
   location->institutionFullName[0]  = '\0';

   location->centerCode              = -1;
   location->centerShortName         = soap_malloc (soap,1);
   location->centerShortName[0]      = '\0';
   location->centerFullName          = soap_malloc (soap,1);
   location->centerFullName[0]       = '\0';

   location->buildingCode            = -1;
   location->buildingShortName       = soap_malloc (soap,1);
   location->buildingShortName[0]    = '\0';
   location->buildingFullName        = soap_malloc (soap,1);
   location->buildingFullName[0]     = '\0';

   location->floor = 0;

   location->roomCode                = -1;
   location->roomShortName           = soap_malloc (soap,1);
   location->roomShortName[0]        = '\0';
   location->roomFullName            = soap_malloc (soap,1);
   location->roomFullName[0]         = '\0';
  }
