// 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-2019 Antonio Caņas 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 <linux/stddef.h>	// For NULL
#include <sys/stat.h>		// For lstat
#include <string.h>
#include <stdsoap2.h>

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

#include "swad_account.h"
#include "swad_API.h"
#include "swad_database.h"
#include "swad_file_browser.h"
#include "swad_global.h"
#include "swad_hierarchy.h"
#include "swad_ID.h"
#include "swad_notice.h"
#include "swad_notification.h"
#include "swad_password.h"
#include "swad_search.h"
#include "swad_user.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] =
  {
   "?",				// 0 ==> unknown function
   "loginBySession",		// 1
   "loginByUserPassword",	// 2 (deprecated)
   "loginByUserPasswordKey",	// 3
   "getCourses",		// 4
   "getUsers",			// 5
   "getNotifications",		// 6
   "getTestConfig",		// 7
   "getTests",			// 8
   "sendMessage",		// 9
   "sendNotice",		// 10
   "getDirectoryTree",		// 11
   "getGroups",			// 12
   "getGroupTypes",		// 13
   "sendMyGroups",		// 14
   "getFile",			// 15
   "markNotificationsAsRead",	// 16
   "getNewPassword",		// 17
   "getCourseInfo",		// 18
   "getAttendanceEvents",	// 19
   "sendAttendanceEvent",	// 20
   "getAttendanceUsers",	// 21
   "sendAttendanceUsers",	// 22
   "createAccount",		// 23
   "getMarks",			// 24
   "getTrivialQuestion",	// 25
   "findUsers",			// 26
   "removeAttendanceEvent",	// 27
   "getGames",			// 28
  };

/* 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
   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 */
Rol_Role_t API_SvcRole_to_RolRole[API_NUM_ROLES] =
  {
   Rol_UNK,	// API_ROLE_UNKNOWN
   Rol_GST,	// API_ROLE__GUEST_
   Rol_STD,	// API_ROLE_STUDENT
   Rol_TCH,	// API_ROLE_TEACHER	// TODO: Create new web service role for non-editing teachers
  };

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

#define API_BYTES_WS_KEY Cry_BYTES_ENCRYPTED_STR_SHA256_BASE64

/*****************************************************************************/
/********************************* Data types ********************************/
/*****************************************************************************/

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

static int API_GetPlgCodFromAppKey (const char *appKey);
static int API_CheckIdSession (const char *IdSession);
static int API_CheckWSKey (char WSKey[API_BYTES_WS_KEY + 1]);

static int API_CheckCourseAndGroupCodes (long CrsCod,long GrpCod);
static int API_GenerateNewWSKey (long UsrCod,
                                 char WSKey[API_BYTES_WS_KEY + 1]);
static int API_RemoveOldWSKeys (void);
static int API_GetCurrentDegCodFromCurrentCrsCod (void);
static bool API_GetSomeUsrDataFromUsrCod (struct UsrData *UsrDat,long CrsCod);

static int API_CheckParamsNewAccount (char *NewNicknameWithArroba,	// Input
                                      char NewNicknameWithoutArroba[Nck_MAX_BYTES_NICKNAME_FROM_FORM + 1],	// Output
                                      char *NewEmail,			// Input-output
                                      char *NewPlainPassword,		// Input
                                      char *NewEncryptedPassword);	// Output

static void API_CopyListUsers (Rol_Role_t Role,struct swad__getUsersOutput *getUsersOut);
static void API_CopyUsrData (struct swad__user *Usr,struct UsrData *UsrDat,bool UsrIDIsVisible);

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

static int API_GetMyLanguage (void);

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

static int API_GetTstConfig (long CrsCod);
static int API_GetNumTestQuestionsInCrs (long CrsCod);
static int API_GetTstTags (long CrsCod,struct swad__getTestsOutput *getTestsOut);
static int API_GetTstQuestions (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut);
static int API_GetTstAnswers (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut);
static int API_GetTstQuestionTags (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut);

static void API_GetListGrpsInGameFromDB (long GamCod,char **ListGroups);

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

/*****************************************************************************/
/******* 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);

      soap_end (soap);	// Clean up and remove deserialized data
      soap_free (soap);	// Detach and free runtime context
     }
  }

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

void API_Exit (const char *DetailErrorMessage)
  {
   int ReturnCode = (DetailErrorMessage ? soap_receiver_fault (Gbl.soap,
	                                                       "Error in swad web service",
	                                                       DetailErrorMessage) :
	                                  0);

   soap_end (Gbl.soap);		// Clean up and remove deserialized data
   soap_free (Gbl.soap);	// Detach and free runtime context

   exit (ReturnCode);
  }

/*****************************************************************************/
/****** 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 (const char *appKey)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

   Gbl.WebService.PlgCod = -1L;

   /***** Get number of plugins with a IP address *****/
   if (DB_QuerySELECT (&mysql_res,"can not check application key",
		       "SELECT PlgCod FROM plugins WHERE AppKey='%s'",
		       appKey))	// Session found in table of sessions
     {
      row = mysql_fetch_row (mysql_res);

      Gbl.WebService.PlgCod = Str_ConvertStrCodToLongCod (row[0]);
     }

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

   if (Gbl.WebService.PlgCod < 0)
      return soap_sender_fault (Gbl.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 (const char *IdSession)
  {
   const char *Ptr;
   unsigned i;

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

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

   /***** Check if session identifier is in base64url *****/
   for (Ptr = IdSession;
	*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 (Gbl.soap,
                                   "Bad session identifier",
                                   "The session identifier must contain only base64url characters");
     }

   /***** Query if session identifier already exists in database *****/
   if (DB_QueryCOUNT ("can not get session data",
		      "SELECT COUNT(*) FROM sessions"
		      " WHERE SessionId='%s'",
                      IdSession) != 1)
      return soap_receiver_fault (Gbl.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_CheckWSKey (char WSKey[API_BYTES_WS_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 (DB_QuerySELECT (&mysql_res,"can not get existence of key",
		       "SELECT UsrCod,PlgCod FROM ws_keys WHERE WSKey='%s'",
		       WSKey))	// Session found in table of sessions
     {
      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 (long CrsCod,long GrpCod)
  {
   /***** Check if course code is correct *****/
   if (CrsCod <= 0)
      return soap_sender_fault (Gbl.soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /***** Query if course code already exists in database *****/
   if (DB_QueryCOUNT ("can not get course",
		      "SELECT COUNT(*) FROM courses"
		      " WHERE CrsCod=%ld",
		      CrsCod) != 1)
      return soap_sender_fault (Gbl.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"
     {
      /***** Query if group code already exists in database *****/
      if (DB_QueryCOUNT ("can not get group",
			 "SELECT COUNT(*) FROM crs_grp_types,crs_grp"
			 " WHERE crs_grp_types.CrsCod=%ld"
			 " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
			 " AND crs_grp.GrpCod=%ld",
			 CrsCod,GrpCod) != 1)
         return soap_sender_fault (Gbl.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_GenerateNewWSKey (long UsrCod,
                                 char WSKey[API_BYTES_WS_KEY + 1])
  {
   int ReturnCode;

   /***** Remove expired web service keys *****/
   if ((ReturnCode = API_RemoveOldWSKeys ()) != SOAP_OK)
      return ReturnCode;

   /***** Create a unique name for the key *****/
   Str_Copy (WSKey,Gbl.UniqueNameEncrypted,
             API_BYTES_WS_KEY);

   /***** Check that key does not exist in database *****/
   if (DB_QueryCOUNT ("can not get existence of key",
		      "SELECT COUNT(*) FROM ws_keys"
		      " WHERE WSKey='%s'",
		      WSKey))
      return soap_receiver_fault (Gbl.soap,
	                          "Error when generating key",
	                          "Generated key already existed in database");

   /***** Insert key into database *****/
   DB_QueryINSERT ("can not insert new key",
		   "INSERT INTO ws_keys"
	           " (WSKey,UsrCod,PlgCod,LastTime)"
                   " VALUES"
                   " ('%s',%ld,%ld,NOW())",
		   WSKey,UsrCod,Gbl.WebService.PlgCod);

   return SOAP_OK;
  }

/*****************************************************************************/
/************************ Remove old web service keys ************************/
/*****************************************************************************/

static int API_RemoveOldWSKeys (void)
  {
   char Query[512];

   /***** Remove expired sessions *****/
   /* A session expire when last click (LastTime) is too old,
      or when there was at least one refresh (navigator supports AJAX) and last refresh is too old (browser probably was closed) */
   sprintf (Query,"DELETE LOW_PRIORITY FROM ws_keys"
	          " WHERE LastTime<FROM_UNIXTIME(UNIX_TIMESTAMP()-%lu)",
            Cfg_TIME_TO_DELETE_WEB_SERVICE_KEY);
   if (mysql_query (&Gbl.mysql,Query))
      return soap_receiver_fault (Gbl.soap,
	                          "Can not remove old web service keys from database",
	                          mysql_error (&Gbl.mysql));

   return SOAP_OK;
  }

/*****************************************************************************/
/********************* Get degree code from course code **********************/
/*****************************************************************************/

static int API_GetCurrentDegCodFromCurrentCrsCod (void)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

   /***** Set default degree code *****/
   Gbl.Hierarchy.Deg.DegCod = -1L;

   /***** Check that key does not exist in database *****/
   if (DB_QuerySELECT (&mysql_res,"can not get the degree of a course",
		       "SELECT DegCod FROM courses WHERE CrsCod=%ld",
		       Gbl.Hierarchy.Crs.CrsCod))	// Course found in table of courses
     {
      row = mysql_fetch_row (mysql_res);

      Gbl.Hierarchy.Deg.DegCod = Str_ConvertStrCodToLongCod (row[0]);
     }

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

   return SOAP_OK;
  }

/*****************************************************************************/
/************ Get user's data from database giving a user's code *************/
/*****************************************************************************/
// Return false if UsrDat->UsrCod does not exist ini database

static bool API_GetSomeUsrDataFromUsrCod (struct UsrData *UsrDat,long CrsCod)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

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

   /***** Get some user's data *****/
   if (DB_QuerySELECT (&mysql_res,"can not get user's data",
		       "SELECT Surname1,Surname2,FirstName,Photo,DATE_FORMAT(Birthday,'%%Y%%m%%d')"
		       " FROM usr_data WHERE UsrCod=%ld",
		       UsrDat->UsrCod) != 1)
      return false;

   /* Read some user's data */
   row = mysql_fetch_row (mysql_res);

   /* Get user's name */
   Str_Copy (UsrDat->Surname1,row[0],
             Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);
   Str_Copy (UsrDat->Surname2,row[1],
             Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);
   Str_Copy (UsrDat->FirstName,row[2],
             Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

   /* Get user's photo */
   Str_Copy (UsrDat->Photo,row[3],
             Cry_BYTES_ENCRYPTED_STR_SHA256_BASE64);

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

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

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

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

   /***** Get user's role *****/
   /* Query database */
   if (CrsCod > 0)
     {
      /* Get the role in the given course */
      if (DB_QuerySELECT (&mysql_res,"can not get user's role",
			  "SELECT Role FROM crs_usr"
			  " WHERE CrsCod=%ld AND UsrCod=%ld",
			  CrsCod,UsrDat->UsrCod))	// User belongs to course
	{
	 row = mysql_fetch_row (mysql_res);
	 if (row[0])
	   {
	    if (sscanf (row[0],"%u",&UsrDat->Roles.InCurrentCrs.Role) == 1)
	       UsrDat->Roles.InCurrentCrs.Valid = true;
	    else
	      {
	       UsrDat->Roles.InCurrentCrs.Role = Rol_UNK;
	       UsrDat->Roles.InCurrentCrs.Valid = false;
	      }
	   }
	 else	// Impossible
	   {
	    UsrDat->Roles.InCurrentCrs.Role = Rol_UNK;
	    UsrDat->Roles.InCurrentCrs.Valid = false;
	   }
	}
      else	// User does not belong to course
	{
	 UsrDat->Roles.InCurrentCrs.Role = Rol_UNK;
	 UsrDat->Roles.InCurrentCrs.Valid = true;
	}
     }
   else
     {
      /* Get the maximum role in any course */
      if (DB_QuerySELECT (&mysql_res,"can not get user's role",
			  "SELECT MAX(Role)"
			  " FROM crs_usr WHERE UsrCod=%ld",
			  UsrDat->UsrCod) == 1)
	{
	 row = mysql_fetch_row (mysql_res);
	 if (row[0])
	   {
	    if (sscanf (row[0],"%u",&UsrDat->Roles.InCurrentCrs.Role) == 1)
	       UsrDat->Roles.InCurrentCrs.Valid = true;
	    else
	      {
	       UsrDat->Roles.InCurrentCrs.Role = Rol_UNK;
	       UsrDat->Roles.InCurrentCrs.Valid = false;
	      }
	   }
	 else
	    // MAX(Role) == NULL if user does not belong to any course
	   {
	    UsrDat->Roles.InCurrentCrs.Role = Rol_UNK;
	    UsrDat->Roles.InCurrentCrs.Valid = true;
	   }
	}
      else	// Impossible
	{
	 UsrDat->Roles.InCurrentCrs.Role = Rol_UNK;
         UsrDat->Roles.InCurrentCrs.Valid = false;
	}
     }

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

   return true;
  }

/*****************************************************************************/
/**************************** 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 NewNicknameWithoutArroba[Nck_MAX_BYTES_NICKNAME_FROM_FORM + 1];
   char NewEncryptedPassword[Pwd_BYTES_ENCRYPTED_PASSWORD + 1];
   int Result;
   int ReturnCode;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_createAccount;

   /***** Allocate space for strings *****/
   createAccountOut->wsKey = (char *) soap_malloc (Gbl.soap,API_BYTES_WS_KEY + 1);

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

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

   /***** Check parameters used to create the new account *****/
   Result = API_CheckParamsNewAccount (userNickname,		// Input
                                       NewNicknameWithoutArroba,// 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,
             Pwd_BYTES_ENCRYPTED_PASSWORD);

   /***** User does not exist in the platform, so create him/her! *****/
   Acc_CreateNewUsr (&Gbl.Usrs.Me.UsrDat,
                     true);	// I am creating my own account

   /***** Save nickname *****/
   Nck_UpdateNickInDB (Gbl.Usrs.Me.UsrDat.UsrCod,NewNicknameWithoutArroba);
   Str_Copy (Gbl.Usrs.Me.UsrDat.Nickname,NewNicknameWithoutArroba,
             Nck_MAX_BYTES_NICKNAME_WITHOUT_ARROBA);

   /***** Save email *****/
   if (Mai_UpdateEmailInDB (&Gbl.Usrs.Me.UsrDat,userEmail))
     {
      /* Email updated sucessfully */
      Str_Copy (Gbl.Usrs.Me.UsrDat.Email,userEmail,
                Cns_MAX_BYTES_EMAIL_ADDRESS);
      Gbl.Usrs.Me.UsrDat.EmailConfirmed = false;
     }

   /***** 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_GenerateNewWSKey ((long) createAccountOut->userCode,
				createAccountOut->wsKey);
  }

/*****************************************************************************/
/************* Get parameters for the creation of a new account **************/
/*****************************************************************************/
// Return false on error
//char *userNickname,char *userEmail,char *userID,char *userPassword
static int API_CheckParamsNewAccount (char *NewNicknameWithArroba,	// Input
                                      char NewNicknameWithoutArroba[Nck_MAX_BYTES_NICKNAME_FROM_FORM + 1],	// Output
                                      char *NewEmail,			// Input-output
                                      char *NewPlainPassword,		// Input
                                      char *NewEncryptedPassword)	// Output
  {
   /***** Step 1/3: Check new nickname *****/
   /* Make a copy without possible starting arrobas */
   Str_Copy (NewNicknameWithoutArroba,NewNicknameWithArroba,
             Nck_MAX_BYTES_NICKNAME_FROM_FORM);
   if (Nck_CheckIfNickWithArrobaIsValid (NewNicknameWithArroba))        // If new nickname is valid
     {
      /***** Remove arrobas at the beginning *****/
      Str_RemoveLeadingArrobas (NewNicknameWithoutArroba);

      /***** Check if the new nickname matches any of the nicknames of other users *****/
      if (DB_QueryCOUNT ("can not check if nickname already existed",
			 "SELECT COUNT(*) FROM usr_nicknames"
			 " WHERE Nickname='%s'",
			 NewNicknameWithoutArroba))	// A nickname of another user is the same that this nickname
	 return API_CHECK_NEW_ACCOUNT_NICKNAME_REGISTERED_BY_ANOTHER_USER;
     }
   else        // New nickname is not valid
      return API_CHECK_NEW_ACCOUNT_NICKNAME_NOT_VALID;

   /***** Step 2/3: Check new email *****/
   if (Mai_CheckIfEmailIsValid (NewEmail))	// New email is valid
     {
      /***** Check if the new email matches any of the confirmed emails of other users *****/
      if (DB_QueryCOUNT ("can not check if email already existed",
			 "SELECT COUNT(*) FROM usr_emails"
			 " WHERE E_mail='%s' AND Confirmed='Y'",
			 NewEmail))	// An email of another user is the same that my email
	 return API_CHECK_NEW_ACCOUNT_EMAIL_REGISTERED_BY_ANOTHER_USER;
     }
   else	// New email is not valid
      return API_CHECK_NEW_ACCOUNT_EMAIL_NOT_VALID;

   /***** Step 3/3: Check new password *****/
   Cry_EncryptSHA512Base64 (NewPlainPassword,NewEncryptedPassword);
   if (!Pwd_SlowCheckIfPasswordIsGood (NewPlainPassword,NewEncryptedPassword,-1L))        // 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_EMAIL_ADDRESS + 1];
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRows;
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];
   bool UsrFound;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_loginByUserPasswordKey;

   /***** Allocate space for strings *****/
   loginByUserPasswordKeyOut->wsKey          = (char *) soap_malloc (Gbl.soap,API_BYTES_WS_KEY + 1);
   loginByUserPasswordKeyOut->userNickname   = (char *) soap_malloc (Gbl.soap,Nck_MAX_BYTES_NICKNAME_WITHOUT_ARROBA + 1);
   loginByUserPasswordKeyOut->userID         = (char *) soap_malloc (Gbl.soap,ID_MAX_BYTES_USR_ID + 1);
   loginByUserPasswordKeyOut->userFirstname  = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginByUserPasswordKeyOut->userSurname1   = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginByUserPasswordKeyOut->userSurname2   = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginByUserPasswordKeyOut->userPhoto      = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_WWW + 1);
   loginByUserPasswordKeyOut->userBirthday   = (char *) soap_malloc (Gbl.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 ((const char *) appKey)) != SOAP_OK)
      return ReturnCode;

   /***** Check if user's email, @nickname or ID are valid *****/
   Str_Copy (UsrIDNickOrEmail,userID,
             Cns_MAX_BYTES_EMAIL_ADDRESS);
   if (Nck_CheckIfNickWithArrobaIsValid (UsrIDNickOrEmail))		// 1: It's a nickname
     {
      Str_RemoveLeadingArrobas (UsrIDNickOrEmail);

      /* User has typed a nickname */
      NumRows =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's data",
				 "SELECT usr_nicknames.UsrCod"
				 " FROM usr_nicknames,usr_data"
				 " WHERE usr_nicknames.Nickname='%s'"
				 " AND usr_nicknames.UsrCod=usr_data.UsrCod"
				 " AND usr_data.Password='%s'",
				 UsrIDNickOrEmail,userPassword);
     }
   else if (Mai_CheckIfEmailIsValid (Gbl.Usrs.Me.UsrIdLogin))		// 2: It's an email
     {
      /* User has typed an email */
      // TODO: Get only if email confirmed?
      NumRows =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's data",
				 "SELECT usr_emails.UsrCod"
				 " FROM usr_emails,usr_data"
				 " WHERE usr_emails.E_mail='%s'"
				 " AND usr_emails.UsrCod=usr_data.UsrCod"
				 " AND usr_data.Password='%s'",
				 UsrIDNickOrEmail,userPassword);
     }
   else									// 3: It's not a nickname nor email
     {
      // Users' IDs are always stored internally in capitals and without leading zeros
      Str_RemoveLeadingZeros (UsrIDNickOrEmail);
      Str_ConvertToUpperText (UsrIDNickOrEmail);
      if (ID_CheckIfUsrIDIsValid (UsrIDNickOrEmail))
	{
	 /* User has typed a valid user's ID (existing or not) */
	 // TODO: Get only if ID confirmed?
	 NumRows =
	 (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's data",
				    "SELECT usr_IDs.UsrCod"
				    " FROM usr_IDs,usr_data"
				    " WHERE usr_IDs.UsrID='%s'"
				    " AND usr_IDs.UsrCod=usr_data.UsrCod"
				    " AND usr_data.Password='%s'",
				    UsrIDNickOrEmail,userPassword);
	}
      else	// String is not a valid user's nickname, email or ID
	 return soap_receiver_fault (Gbl.soap,
				     "Bad log in",
				     "User's ID or nickname don't exist or password is wrong");
     }

   /***** Get user's data from database *****/
   if (NumRows == 1)	// User found in table of users' data
     {
      row = mysql_fetch_row (mysql_res);

      /***** Get user code (row[0]) *****/
      Gbl.Usrs.Me.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[0]);

      /***** Get user's data *****/
      UsrFound = API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,-1L);	// Get some user's data from database
     }
   else
      UsrFound = false;

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

   if (UsrFound)
     {
      Gbl.Usrs.Me.Logged = true;
      Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role;

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

      Str_Copy (loginByUserPasswordKeyOut->userNickname,
                Gbl.Usrs.Me.UsrDat.Nickname,
                Nck_MAX_BYTES_NICKNAME_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.FirstName,
                Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

      Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,PhotoURL);
      Str_Copy (loginByUserPasswordKeyOut->userPhoto,PhotoURL,
                Cns_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.Role];

      /***** Generate a key used in subsequents calls to other web services *****/
      return API_GenerateNewWSKey ((long) loginByUserPasswordKeyOut->userCode,
                                   loginByUserPasswordKeyOut->wsKey);
     }
   else
     {
      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 (Gbl.soap,
	                          "Bad log in",
	                          "User's ID or nickname don't exist or password is wrong");
     }
  }

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

int swad__loginBySessionKey (struct soap *soap,
                             char *sessionID,char *appKey,					// input
                             struct swad__loginBySessionKeyOutput *loginBySessionKeyOut)	// output
  {
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRows;
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];
   bool UsrFound;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_loginBySessionKey;

   /***** Allocate space for strings *****/
   loginBySessionKeyOut->wsKey          = (char *) soap_malloc (Gbl.soap,API_BYTES_WS_KEY + 1);
   loginBySessionKeyOut->userNickname   = (char *) soap_malloc (Gbl.soap,Nck_MAX_BYTES_NICKNAME_WITHOUT_ARROBA + 1);
   loginBySessionKeyOut->userID         = (char *) soap_malloc (Gbl.soap,ID_MAX_BYTES_USR_ID + 1);
   loginBySessionKeyOut->userFirstname  = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginBySessionKeyOut->userSurname1   = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginBySessionKeyOut->userSurname2   = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
   loginBySessionKeyOut->userPhoto      = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_WWW + 1);
   loginBySessionKeyOut->userBirthday   = (char *) soap_malloc (Gbl.soap,Dat_LENGTH_YYYYMMDD + 1);
   loginBySessionKeyOut->degreeName     = (char *) soap_malloc (Gbl.soap,Hie_MAX_BYTES_FULL_NAME + 1);
   loginBySessionKeyOut->courseName     = (char *) soap_malloc (Gbl.soap,Hie_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 ((const char *) appKey)) != SOAP_OK)
      return ReturnCode;

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

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

   // Now, we know that sessionID is a valid session identifier
   /***** Query data of the session from database *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get session data",
			      "SELECT UsrCod,DegCod,CrsCod FROM sessions"
			      " WHERE SessionId='%s'",
			      sessionID);
   if (NumRows == 1)	// Session found in table of sessions
     {
      row = mysql_fetch_row (mysql_res);

      /***** Get course (row[2]) *****/
      Gbl.Hierarchy.Crs.CrsCod = Str_ConvertStrCodToLongCod (row[2]);
      Crs_GetDataOfCourseByCod (&Gbl.Hierarchy.Crs);
      loginBySessionKeyOut->courseCode = (int) Gbl.Hierarchy.Crs.CrsCod;
      Str_Copy (loginBySessionKeyOut->courseName,Gbl.Hierarchy.Crs.FullName,
                Hie_MAX_BYTES_FULL_NAME);

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

      /***** Get degree (row[1]) *****/
      Gbl.Hierarchy.Deg.DegCod = Str_ConvertStrCodToLongCod (row[1]);
      Deg_GetDataOfDegreeByCod (&Gbl.Hierarchy.Deg);
      loginBySessionKeyOut->degreeCode = (int) Gbl.Hierarchy.Deg.DegCod;
      Str_Copy (loginBySessionKeyOut->degreeName,Gbl.Hierarchy.Deg.FullName,
                Hie_MAX_BYTES_FULL_NAME);
     }
   else
      UsrFound = false;

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

   /***** Get degree of current course *****/
   if ((ReturnCode = API_GetCurrentDegCodFromCurrentCrsCod ()) != SOAP_OK)
     return ReturnCode;

   if (UsrFound)
     {
      Gbl.Usrs.Me.Logged = true;
      Gbl.Usrs.Me.Role.Logged = Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role;

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

      Str_Copy (loginBySessionKeyOut->userNickname,Gbl.Usrs.Me.UsrDat.Nickname,
                Nck_MAX_BYTES_NICKNAME_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.FirstName,
                Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

      Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,PhotoURL);
      Str_Copy (loginBySessionKeyOut->userPhoto,PhotoURL,
                Cns_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.Role];

      /***** Generate a key used in subsequents calls to other web services *****/
      return API_GenerateNewWSKey ((long) loginBySessionKeyOut->userCode,
                                   loginBySessionKeyOut->wsKey);
     }
   else
      return soap_receiver_fault (Gbl.soap,
	                          "Bad session identifier",
	                          "Session identifier does not exist in database");
  }

/*****************************************************************************/
/*********************** 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_EMAIL_ADDRESS + 1];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRows;
   char NewRandomPlainPassword[Pwd_MAX_BYTES_PLAIN_PASSWORD + 1];

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getNewPassword;

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

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

   /***** Check if user's email, @nickname or ID are valid *****/
   Str_Copy (UsrIDNickOrEmail,userID,
             Cns_MAX_BYTES_EMAIL_ADDRESS);
   if (Nck_CheckIfNickWithArrobaIsValid (UsrIDNickOrEmail))		// 1: It's a nickname
     {
      Str_RemoveLeadingArrobas (UsrIDNickOrEmail);

      /* User has typed a nickname */
      NumRows =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's data",
				 "SELECT UsrCod FROM usr_nicknames"
				 " WHERE Nickname='%s'",
				 UsrIDNickOrEmail);
     }
   else if (Mai_CheckIfEmailIsValid (Gbl.Usrs.Me.UsrIdLogin))		// 2: It's an email
     {
      /* User has typed an email */
      // TODO: Get only if email confirmed?
      NumRows =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's data",
				 "SELECT UsrCod FROM usr_emails"
				 " WHERE E_mail='%s'",
				 UsrIDNickOrEmail);
     }
   else									// 3: It's not a nickname nor email
     {
      // Users' IDs are always stored internally in capitals and without leading zeros
      Str_RemoveLeadingZeros (UsrIDNickOrEmail);
      Str_ConvertToUpperText (UsrIDNickOrEmail);
      if (ID_CheckIfUsrIDIsValid (UsrIDNickOrEmail))
	{
	 /* User has typed a valid user's ID (existing or not) */
	 // TODO: Get only if ID confirmed?
	 NumRows =
	 (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's data",
				    "SELECT UsrCod FROM usr_IDs"
				    " WHERE UsrID='%s'",
				    UsrIDNickOrEmail);
	}
      else	// String is not a valid user's nickname, email or ID
	 return soap_receiver_fault (Gbl.soap,
				     "Bad log in",
				     "User's email, nickname or ID don't exist");
     }

   /***** Get user's data from database *****/
   if (NumRows == 1)	// One unique user found in table of users' data
     {
      row = mysql_fetch_row (mysql_res);

      /***** Get user code (row[0]) *****/
      Gbl.Usrs.Me.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[0]);

      Usr_GetUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Usr_DONT_GET_PREFS);	// Get my data

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

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

   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 NumRow;
   unsigned NumRows;
   Rol_Role_t Role;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getCourses;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Query my courses from database *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's courses",
			      "SELECT courses.CrsCod,"
				     "courses.ShortName,"
				     "courses.FullName,"
				     "crs_usr.Role"
			      " FROM crs_usr,courses"
			      " WHERE crs_usr.UsrCod=%ld"
			      " AND crs_usr.CrsCod=courses.CrsCod"
			      " ORDER BY courses.FullName",
			      Gbl.Usrs.Me.UsrDat.UsrCod);

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

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

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

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

         /* Get course short name (row[1]) */
         getCoursesOut->coursesArray.__ptr[NumRow].courseShortName = (char *) soap_malloc (Gbl.soap,Hie_MAX_BYTES_SHRT_NAME + 1);
	 Str_Copy (getCoursesOut->coursesArray.__ptr[NumRow].courseShortName,row[1],
	           Hie_MAX_BYTES_SHRT_NAME);

         /* Get course full name (row[2]) */
         getCoursesOut->coursesArray.__ptr[NumRow].courseFullName = (char *) soap_malloc (Gbl.soap,Hie_MAX_BYTES_FULL_NAME + 1);
	 Str_Copy (getCoursesOut->coursesArray.__ptr[NumRow].courseFullName,row[2],
	           Hie_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[NumRow].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;
   Inf_InfoType_t InfoType;
   size_t Length;
   Inf_InfoSrc_t InfoSrc;
   bool MustBeRead;
   int Result = SOAP_OK;
   const char *NamesInWSForInfoType[Inf_NUM_INFO_TYPES] =
     {
      "introduction",	// Inf_INTRODUCTION
      "guide",		// Inf_TEACHING_GUIDE
      "lectures",	// Inf_LECTURES
      "practicals",	// Inf_PRACTICALS
      "bibliography",	// Inf_BIBLIOGRAPHY
      "FAQ",		// Inf_FAQ
      "links",		// Inf_LINKS
      "assessment",	// Inf_ASSESSMENT
     };
   const char *NamesInWSForInfoSrc[Inf_NUM_INFO_SOURCES] =
     {
      "none",		// Inf_INFO_SRC_NONE
      "editor",		// Inf_INFO_SRC_EDITOR
      "plainText",	// Inf_INFO_SRC_PLAIN_TEXT
      "richText",	// Inf_INFO_SRC_RICH_TEXT
      "page",		// Inf_INFO_SRC_PAGE
      "URL",		// Inf_INFO_SRC_URL
     };

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getCourseInfo;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

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

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (Gbl.Hierarchy.Crs.CrsCod,-1L)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Get info source *****/
   for (InfoType = (Inf_InfoType_t) 0;
	InfoType < Inf_NUM_INFO_TYPES;
	InfoType++)
      if (!strcmp (infoType,NamesInWSForInfoType[InfoType]))
	 break;
   if (InfoType == Inf_NUM_INFO_TYPES)	// Not found!
      return soap_receiver_fault (Gbl.soap,
	                          "Bad info type",
	                          "Unknown requested info type");
   Gbl.Crs.Info.Type = InfoType;
   Inf_GetAndCheckInfoSrcFromDB (Gbl.Hierarchy.Crs.CrsCod,Gbl.Crs.Info.Type,&InfoSrc,&MustBeRead);
   Length = strlen (NamesInWSForInfoSrc[InfoSrc]);
   getCourseInfo->infoSrc = (char *) soap_malloc (Gbl.soap,Length + 1);
   Str_Copy (getCourseInfo->infoSrc,NamesInWSForInfoSrc[InfoSrc],
             Length);

   /***** Set paths *****/
   Hie_InitHierarchy ();

   /***** Get info text *****/
   getCourseInfo->infoTxt = NULL;
   switch (InfoSrc)
     {
      case Inf_INFO_SRC_NONE:		// No info available
         break;
      case Inf_INFO_SRC_EDITOR:		// Internal editor (only for syllabus)
	 switch (Gbl.Crs.Info.Type)
	   {
	    case Inf_LECTURES:		// Syllabus (lectures)
	    case Inf_PRACTICALS:	// Syllabys (practicals)
	       Result = Syl_WriteSyllabusIntoHTMLBuffer (&(getCourseInfo->infoTxt));
	       break;
	    default:
               break;
	   }
	 break;
      case Inf_INFO_SRC_PLAIN_TEXT:	// Plain text
      case Inf_INFO_SRC_RICH_TEXT:	// Rich text (not yet available)
	 Result = Inf_WritePlainTextIntoHTMLBuffer (&(getCourseInfo->infoTxt));
         break;
      case Inf_INFO_SRC_PAGE:		// Web page hosted in SWAD server
	 Result = Inf_WritePageIntoHTMLBuffer (&(getCourseInfo->infoTxt));
         break;
      case Inf_INFO_SRC_URL:		// Link to a web page
         getCourseInfo->infoTxt = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_WWW + 1);
         Inf_WriteURLIntoTxtBuffer (getCourseInfo->infoTxt);
         break;
     }

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

   return Result;
  }

/*****************************************************************************/
/************* 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 *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getUsers;
   Gbl.Hierarchy.Crs.CrsCod = (courseCode > 0) ? (long) courseCode :
	                                          -1L;

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

   /***** Check course *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (Gbl.Hierarchy.Crs.CrsCod,-1L)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Get degree of current course *****/
   if ((ReturnCode = API_GetCurrentDegCodFromCurrentCrsCod ()) != SOAP_OK)	// TODO: Is this necessary?
     return ReturnCode;

   /***** Check requested users' role *****/
   if (userRole != API_ROLE_STUDENT &&	// Students
       userRole != API_ROLE_TEACHER)	// Teachers
      return soap_sender_fault (Gbl.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_GetListGrpTypesInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

   /***** Get list of users *****/
   Usr_GetListUsrs (Hie_CRS,Role);
   API_CopyListUsers (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;
   char SearchQuery[Sch_MAX_BYTES_SEARCH_QUERY + 1];
   Rol_Role_t Role;
   bool FilterTooShort = false;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_findUsers;
   Gbl.Hierarchy.Crs.CrsCod = (courseCode > 0) ? (long) courseCode :
	                                          -1L;

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

   if (Gbl.Hierarchy.Level == Hie_CRS)	// Course selected
      /***** Check course *****/
      if ((ReturnCode = API_CheckCourseAndGroupCodes (Gbl.Hierarchy.Crs.CrsCod,-1L)) != SOAP_OK)
	 return ReturnCode;

   /***** Get some of my data *****/
   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   if (Gbl.Hierarchy.Level == 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.Role != Rol_STD &&
          Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_NET &&
	  Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_TCH)
	 return soap_receiver_fault (Gbl.soap,
				     "Request forbidden",
				     "Requester must belong to course");

   if (Gbl.Hierarchy.Level == Hie_CRS)
     {
      /***** Get degree of current course *****/
      if ((ReturnCode = API_GetCurrentDegCodFromCurrentCrsCod ()) != SOAP_OK)	// TODO: Is this necessary?
	return ReturnCode;
     }

   /***** Check requested users' role *****/
   if (userRole < API_ROLE_UNKNOWN ||
       userRole > API_ROLE_TEACHER)
      return soap_sender_fault (Gbl.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 *****/
   Str_Copy (Gbl.Search.Str,filter,
             Sch_MAX_BYTES_STRING_TO_FIND);

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

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

	 /***** Drop temporary table with candidate users *****/
	 Usr_DropTmpTableWithCandidateUsrs ();
        }
      else
	 FilterTooShort = true;
     }
   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 (Rol_Role_t Role,struct swad__getUsersOutput *getUsersOut)
  {
   unsigned NumUsrs;
   unsigned NumUsr;
   struct UsrData UsrDat;
   bool 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 (Gbl.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_GetNicknameFromUsrCod (UsrDat.UsrCod,UsrDat.Nickname);

	 /* Copy user's data into output structure */
	 API_CopyUsrData (&(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 NumRow;
   unsigned NumRows;
   long OpenTime;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getGroupTypes;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

   /***** 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_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Query group types in a course from database *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get group types",
			      "SELECT GrpTypCod,"
			             "GrpTypName,"
			             "Mandatory,"
			             "Multiple,"
			             "UNIX_TIMESTAMP(OpenTime)"
			      " FROM crs_grp_types"
			      " WHERE CrsCod=%d"
			      " ORDER BY GrpTypName",
			      courseCode);

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

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

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

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

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

         /* Get whether enrolment is mandatory ('Y') or voluntary ('N') (row[2]) */
         getGroupTypesOut->groupTypesArray.__ptr[NumRow].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[NumRow].multiple = (row[3][0] == 'Y') ? 1 :
                                                                                         0;

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

   /***** 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 NumRow,NumRows;
   long GrpCod;
   unsigned MaxStudents;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getGroups;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

   /***** 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_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.soap,
	                          "Bad web service key",
	                          "Web service key does not exist in database");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Query groups in a course from database *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's groups",
			      "SELECT crs_grp_types.GrpTypCod,"
				     "crs_grp_types.GrpTypName,"
				     "crs_grp.GrpCod,"
				     "crs_grp.GrpName,"
				     "crs_grp.MaxStudents,"
				     "crs_grp.Open,"
				     "crs_grp.FileZones"
			      " FROM crs_grp_types,crs_grp"
			      " WHERE crs_grp_types.CrsCod=%d"
			      " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
			      " ORDER BY crs_grp_types.GrpTypName,"
			      "crs_grp.GrpName",
			      courseCode);

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

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

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

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

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

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

         /* Get group name (row[3]) */
         getGroupsOut->groupsArray.__ptr[NumRow].groupName = (char *) soap_malloc (Gbl.soap,Grp_MAX_BYTES_GROUP_NAME + 1);
	 Str_Copy (getGroupsOut->groupsArray.__ptr[NumRow].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[NumRow].maxStudents = (MaxStudents > Grp_MAX_STUDENTS_IN_A_GROUP) ? -1 :
                                                                                                             (int) MaxStudents;

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

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

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

         /* Get whether I belong to this group or not */
         getGroupsOut->groupsArray.__ptr[NumRow].member = Grp_GetIfIBelongToGrp (GrpCod) ? 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[1 + 10 + 1];
   unsigned NumGrp;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow;
   unsigned NumRows;
   long GrpCod;
   unsigned MaxStudents;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_sendMyGroups;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

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

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check if I am a student, non-editing teacher or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_TCH)
      return soap_receiver_fault (Gbl.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,1 + 10);
      LstGrpsIWant.NumGrps = NumGrp;

      if (LstGrpsIWant.NumGrps)	// If I have selected groups...
        {
         /***** Create a list of groups selected from myGroups *****/
         if ((LstGrpsIWant.GrpCods = (long *) calloc (LstGrpsIWant.NumGrps,sizeof (long))) == NULL)
	    Lay_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,1 + 10);
	    LstGrpsIWant.GrpCods[NumGrp] = Str_ConvertStrCodToLongCod (LongStr);
	   }
        }
     }

   /***** Change my groups *****/
   SendMyGroupsOut->success = Grp_ChangeMyGrpsAtomically (&LstGrpsIWant);

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

   /***** Query groups in a course from database *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's groups",
			      "SELECT crs_grp_types.GrpTypCod,"
				     "crs_grp_types.GrpTypName,"
				     "crs_grp.GrpCod,"
				     "crs_grp.GrpName,"
				     "crs_grp.MaxStudents,"
				     "crs_grp.Open,"
				     "crs_grp.FileZones"
			      " FROM crs_grp_types,crs_grp"
			      " WHERE crs_grp_types.CrsCod=%d"
			      " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
			      " ORDER BY crs_grp_types.GrpTypName,crs_grp.GrpName",
			      courseCode);

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

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

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

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

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

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

         /* Get group name (row[3]) */
         SendMyGroupsOut->groupsArray.__ptr[NumRow].groupName = (char *) soap_malloc (Gbl.soap,Grp_MAX_BYTES_GROUP_NAME + 1);
	 Str_Copy (SendMyGroupsOut->groupsArray.__ptr[NumRow].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[NumRow].maxStudents = (MaxStudents > Grp_MAX_STUDENTS_IN_A_GROUP) ? -1 :
                                                                                                                (int) MaxStudents;

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

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

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

         /* Get whether I belong to this group or not */
         SendMyGroupsOut->groupsArray.__ptr[NumRow].member = Grp_GetIfIBelongToGrp (GrpCod) ? 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 swad__user *Usr,struct UsrData *UsrDat,bool UsrIDIsVisible)
  {
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];
   const char *FirstID;
   size_t Length;

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

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

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

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

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

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

   /* User's photo URL */
   Pho_BuildLinkToPhoto (UsrDat,PhotoURL);
   Length = strlen (PhotoURL);
   Usr->userPhoto = (char *) soap_malloc (Gbl.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 NumRows;
   int NumAttEvent;
   long AttCod;
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];
   long StartTime;
   long EndTime;
   size_t Length;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getAttendanceEvents;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

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

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Query list of attendance events *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get attendance events",
			      "SELECT AttCod,Hidden,UsrCod,"
			      "UNIX_TIMESTAMP(StartTime) AS ST,"
			      "UNIX_TIMESTAMP(EndTime) AS ET,"
			      "CommentTchVisible,Title,Txt"
			      " FROM att_events"
			      " WHERE CrsCod=%d"
			      " ORDER BY ST DESC,ET DESC,Title DESC",
			      courseCode);

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

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

      for (NumAttEvent = 0;
	   NumAttEvent < getAttendanceEventsOut->numEvents;
	   NumAttEvent++)
	{
	 /* Get next group */
	 row = mysql_fetch_row (mysql_res);

	 /* Get attendance event code (row[0]) */
	 AttCod = Str_ConvertStrCodToLongCod (row[0]);
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].attendanceEventCode = (int) AttCod;

         /* Get whether the attendance event is hidden or not (row[1]) */
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].hidden = (row[1][0] == 'Y') ? 1 :
										              0;

	 /* Get user's code of the user who created the event (row[2]) */
         Gbl.Usrs.Other.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[2]);
         if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,Gbl.Hierarchy.Crs.CrsCod))	// Get some user's data from database
           {
            Length = strlen (Gbl.Usrs.Other.UsrDat.Surname1);
            getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname1 = (char *) soap_malloc (Gbl.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 = (char *) soap_malloc (Gbl.soap,Length + 1);
            Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname2,
                      Gbl.Usrs.Other.UsrDat.Surname2,
                      Length);

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

            Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
            Length = strlen (PhotoURL);
            getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto = (char *) soap_malloc (Gbl.soap,Length + 1);
            Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto,
                      PhotoURL,
                      Length);
           }
         else
           {
            getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname1  = NULL;
            getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname2  = NULL;
            getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userFirstname = NULL;
            getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto     = NULL;
           }

	 /* Get event start time (row[3]) */
         StartTime = 0L;
         if (row[3])
            sscanf (row[3],"%ld",&StartTime);
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].startTime = StartTime;

	 /* Get event end time (row[4]) */
         EndTime = 0L;
         if (row[4])
            sscanf (row[4],"%ld",&EndTime);
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].endTime = EndTime;

         /* Get whether teachers comments are visible ('Y') or hidden ('N') (row[5]) */
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].commentsTeachersVisible = (row[5][0] == 'Y') ? 1 :
                                                                                                               0;

	 /* Get title of the event (row[6]) */
         Length = strlen (row[6]);
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].title = (char *) soap_malloc (Gbl.soap,Length + 1);
         Str_Copy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].title,row[6],
                   Length);

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

	 /* Get list of groups for this attendance event */
	 API_GetListGrpsInAttendanceEventFromDB (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 (long AttCod,char **ListGroups)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   long NumGrps;
   long NumGrp;
   long GrpCod;
   char GrpCodStr[10 + 1];
   size_t Length;

   /***** Get list of groups *****/
   NumGrps =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get groups"
					 " of an attendance event",
			      "SELECT GrpCod FROM att_grp WHERE AttCod=%ld",
			      AttCod);
   if (NumGrps == 0)
      *ListGroups = NULL;
   else	// Events found
     {
      Length = NumGrps * (10 + 1) - 1;
      *ListGroups = soap_malloc (Gbl.soap,Length + 1);
      (*ListGroups)[0] = '\0';

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

	 /* Get group code (row[0]) */
	 GrpCod = Str_ConvertStrCodToLongCod (row[0]);
	 snprintf (GrpCodStr,sizeof (GrpCodStr),
		   NumGrp ? ",%ld" :
			    "%ld",
		   GrpCod);
	 Str_Concat (*ListGroups,GrpCodStr,
	             Length);
	}
     }

   /***** 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 AttendanceEvent Att;
   bool ItsANewAttEvent;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_sendAttendanceEvent;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

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

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

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

   /* Course code */
   if (Att.AttCod > 0)	// The event already exists
     {
      Att_GetDataOfAttEventByCod (&Att);
      if (Att.CrsCod != (long) courseCode)
	 return soap_receiver_fault (Gbl.soap,
				     "Request forbidden",
				     "Attendance event does not belong to course");
      ItsANewAttEvent = false;
     }
   else
     {
      ItsANewAttEvent = true;
      Att.CrsCod = (long) courseCode;
     }

   /* Is event hidden? */
   Att.Hidden = (hidden ? true :
	                  false);

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

   /* startTime */
   Att.TimeUTC[Att_START_TIME] = (time_t) startTime;

   /* endTime */
   Att.TimeUTC[Att_END_TIME  ] = (time_t) endTime;

   /* Are teacher's comments visible? */
   Att.CommentTchVisible = (commentsTeachersVisible ? true :
	                                              false);

   /* Title */
   if (!title[0])
      return soap_receiver_fault (Gbl.soap,
				  "Request forbidden",
				  "Title of attendance event is empty");
   Str_Copy (Att.Title,title,
             Att_MAX_BYTES_ATTENDANCE_EVENT_TITLE);

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

   /***** Create or update attendance event *****/
   if (ItsANewAttEvent)
      Att_CreateAttEvent (&Att,text);	// Add new attendance event to database
   else
      Att_UpdateAttEvent (&Att,text);	// Modify existing attendance event

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

   sendAttendanceEventOut->attendanceEventCode = Att.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 AttendanceEvent Att;

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

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

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

   /* Course code */
   if (Att.AttCod > 0)	// The event already exists
     {
      Att_GetDataOfAttEventByCod (&Att);
      Gbl.Hierarchy.Crs.CrsCod = Att.CrsCod;
     }
   else
      return soap_receiver_fault (Gbl.soap,
				  "Request forbidden",
				  "Attendance event does not exist");

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Remove the attendance event from database *****/
   Att_RemoveAttEventFromDB (Att.AttCod);

   removeAttendanceEventOut->attendanceEventCode = Att.AttCod;

   return SOAP_OK;
  }

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

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

   /***** Count number of groups *****/
   for (Ptr = Groups, NumGrp = 0;
	*Ptr;
	NumGrp++)
      Str_GetNextStringUntilComma (&Ptr,LongStr,1 + 10);
   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 = (long *) calloc (Gbl.Crs.Grps.LstGrpsSel.NumGrps,sizeof (long))) == NULL)
	 Lay_NotEnoughMemoryExit ();

      for (Ptr = Groups, NumGrp = 0;
	   *Ptr;
	   )
	{
	 Str_GetNextStringUntilComma (&Ptr,LongStr,1 + 10);
	 Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrp] = Str_ConvertStrCodToLongCod (LongStr);
	 if (Grp_CheckIfGroupBelongsToCourse (Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrp],Gbl.Hierarchy.Crs.CrsCod))
	    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 AttendanceEvent Att;
   char SubQuery[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];
   size_t Length;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getAttendanceUsers;

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

   /***** Get course of this attendance event *****/
   Att.AttCod = (long) attendanceEventCode;
   Att_GetDataOfAttEventByCod (&Att);
   Gbl.Hierarchy.Crs.CrsCod = Att.CrsCod;

   /***** Get some of my data *****/
   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Query list of attendance users *****/
   if (Att_CheckIfAttEventIsAssociatedToGrps (Att.AttCod))
      // Event for one or more groups
      // Subquery: list of users in groups of this attendance event...
      // ...who have no entry in attendance list of users
      sprintf (SubQuery,"SELECT DISTINCT crs_grp_usr.UsrCod AS UsrCod,'N' AS Present"
		        " FROM att_grp,crs_grp,crs_grp_types,crs_usr,crs_grp_usr"
		        " WHERE att_grp.AttCod=%ld"
		        " AND att_grp.GrpCod=crs_grp.GrpCod"
		        " AND crs_grp.GrpTypCod=crs_grp_types.GrpTypCod"
		        " AND crs_grp_types.CrsCod=crs_usr.CrsCod"
		        " AND crs_usr.Role=%u"
		        " AND crs_usr.UsrCod=crs_grp_usr.UsrCod"
		        " AND crs_grp_usr.GrpCod=att_grp.GrpCod"
		        " AND crs_grp_usr.UsrCod NOT IN"
		        " (SELECT UsrCod FROM att_usr WHERE AttCod=%ld)",
	       Att.AttCod,
	       (unsigned) Rol_STD,
	       Att.AttCod);
   else
      // Event for the whole course
      // Subquery: list of users in the course of this attendance event...
      // ...who have no entry in attendance list of users
      sprintf (SubQuery,"SELECT crs_usr.UsrCod AS UsrCod,'N' AS Present"
		        " FROM att_events,crs_usr"
		        " WHERE att_events.AttCod=%ld"
		        " AND att_events.CrsCod=crs_usr.CrsCod"
		        " AND crs_usr.Role=%u"
		        " AND crs_usr.UsrCod NOT IN"
		        " (SELECT UsrCod FROM att_usr WHERE AttCod=%ld)",
	       Att.AttCod,
	       (unsigned) Rol_STD,
	       Att.AttCod);
   // Query: list of users in attendance list + rest of users (subquery)
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get users"
					 " in an attendance event",
			      "SELECT u.UsrCod,u.Present FROM "
			      "(SELECT UsrCod,Present"
			      " FROM att_usr WHERE AttCod=%ld"
			      " UNION %s) AS u,usr_data"
			      " WHERE u.UsrCod=usr_data.UsrCod"
			      " ORDER BY usr_data.Surname1,"
					"usr_data.Surname2,"
					"usr_data.FirstName",
			      (long) attendanceEventCode,SubQuery);

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

   if (NumRows == 0)
      getAttendanceUsersOut->usersArray.__ptr = NULL;
   else	// Events found
     {
      getAttendanceUsersOut->usersArray.__ptr = soap_malloc (Gbl.soap,(getAttendanceUsersOut->usersArray.__size) * sizeof (*(getAttendanceUsersOut->usersArray.__ptr)));

      for (NumRow = 0;
	   NumRow < NumRows;
	   NumRow++)
	{
	 /* 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[NumRow].userCode = (int) Gbl.Usrs.Other.UsrDat.UsrCod;

         /* Get user's data */
         if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,-1L))	// Get some user's data from database
           {
            Length = strlen (Gbl.Usrs.Other.UsrDat.Nickname);
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userNickname = (char *) soap_malloc (Gbl.soap,Length + 1);
            Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumRow].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[NumRow].userID = (char *) soap_malloc (Gbl.soap,Length + 1);
	       Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumRow].userID,
	                 Gbl.Usrs.Other.UsrDat.IDs.List[0].ID,
	                 Length);
              }
            else
              {
	       getAttendanceUsersOut->usersArray.__ptr[NumRow].userID = (char *) soap_malloc (Gbl.soap,1);
	       getAttendanceUsersOut->usersArray.__ptr[NumRow].userID[0] = '\0';
              }

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

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

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

            Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
            Length = strlen (PhotoURL);
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userPhoto = (char *) soap_malloc (Gbl.soap,Length + 1);
            Str_Copy (getAttendanceUsersOut->usersArray.__ptr[NumRow].userPhoto,
                      PhotoURL,
                      Length);
           }
         else
           {
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userNickname  = NULL;
	    getAttendanceUsersOut->usersArray.__ptr[NumRow].userID        = NULL;
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userSurname1  = NULL;
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userSurname2  = NULL;
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userFirstname = NULL;
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userPhoto     = NULL;
           }

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

   /***** 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 AttendanceEvent Att;
   const char *Ptr;
   char LongStr[1 + 10 + 1];
   struct UsrData UsrDat;
   unsigned NumCodsInList;
   char *SubQueryAllUsrs = NULL;
   char SubQueryOneUsr[1 + 1 + 10 + 1];
   size_t Length = 0;	// Initialized to avoid warning

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_sendAttendanceUsers;

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

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

   /***** Get course of this attendance event *****/
   Att.AttCod = (long) attendanceEventCode;
   if (!Att_GetDataOfAttEventByCod (&Att))
      return SOAP_OK;	// return with success = 0
   Gbl.Hierarchy.Crs.CrsCod = Att.CrsCod;

   /***** Get some of my data *****/
   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

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

   if (setOthersAsAbsent)
     {
      /* Count number of codes in list */
      for (Ptr = users, NumCodsInList = 0;
	   *Ptr;
	   NumCodsInList++)
	 /* Find next string in text until comma (leading and trailing spaces are removed) */
	 Str_GetNextStringUntilComma (&Ptr,LongStr,1 + 10);

      /* Allocate subquery used to mark not present users as absent */
      Length = 256 + NumCodsInList * (1 + 1 + 10 + 1) - 1;
      if ((SubQueryAllUsrs = (char *) malloc (Length + 1)) == NULL)
	 return soap_receiver_fault (Gbl.soap,
	                             "Not enough memory",
	                             "Not enough memory to store list of users");
      SubQueryAllUsrs[0] = '\0';
     }

   for (Ptr = users;
	*Ptr;
	)
     {
      /* Find next string in text until comma (leading and trailing spaces are removed) */
      Str_GetNextStringUntilComma (&Ptr,LongStr,1 + 10);
      if ((UsrDat.UsrCod = Str_ConvertStrCodToLongCod (LongStr)) > 0)
	 if (Usr_ChkIfUsrCodExists (UsrDat.UsrCod))
	    // The user must belong to course,
	    // but it's not necessary he/she belongs to groups associated to the event
	    if (Usr_CheckIfUsrBelongsToCurrentCrs (&UsrDat))
	      {
	       /* Mark user as present */
	       Att_RegUsrInAttEventNotChangingComments (Att.AttCod,UsrDat.UsrCod);

	       /* Add this user to query used to mark not present users as absent */
	       if (setOthersAsAbsent)
		 {
		  if (sendAttendanceUsersOut->numUsers)
		    {
		     snprintf (SubQueryOneUsr,sizeof (SubQueryOneUsr),
			       ",%ld",UsrDat.UsrCod);
		     Str_Concat (SubQueryAllUsrs,SubQueryOneUsr,
		                 Length);
		    }
		  else
		     snprintf (SubQueryAllUsrs,Length,
			       " AND UsrCod NOT IN (%ld",
			       UsrDat.UsrCod);
		 }

	       sendAttendanceUsersOut->numUsers++;
	      }
     }

   if (setOthersAsAbsent)
     {
      /* Mark not present users as absent in table of users */
      if (sendAttendanceUsersOut->numUsers)
         Str_Concat (SubQueryAllUsrs,")",
                     Length);

      DB_QueryUPDATE ("can not set other users as absent",
      		     "UPDATE att_usr SET Present='N'"
		     " WHERE AttCod=%ld%s",
		     Att.AttCod,SubQueryAllUsrs);

      /* Free memory for subquery string */
      free ((void *) SubQueryAllUsrs);

      /* Clean table att_usr */
      Att_RemoveUsrsAbsentWithoutCommentsFromAttEvent (Att.AttCod);
     }

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

   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 const char *Ntf_WSNotifyEvents[Ntf_NUM_NOTIFY_EVENTS];
   extern const char *Txt_Forum;
   extern const char *Txt_Course;
   extern const char *Txt_Degree;
   extern const char *Txt_Centre;
   extern const char *Txt_Institution;
   int ReturnCode;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumNotifications;
   unsigned NumNotif;
   long NtfCod;
   Ntf_NotifyEvent_t NotifyEvent;
   long EventTime;
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];
   struct Instit Ins;
   struct Centre Ctr;
   struct Degree Deg;
   struct Course Crs;
   long Cod;
   struct 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;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getNotifications;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Get my notifications from database *****/
   NumNotifications =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get user's notifications",
			      "SELECT NtfCod,"				// row[0]
				     "NotifyEvent,"			// row[1]
				     "UNIX_TIMESTAMP(TimeNotif),"	// row[2]
				     "FromUsrCod,"			// row[3]
				     "InsCod,"				// row[4]
				     "CtrCod,"				// row[5]
				     "DegCod,"				// row[6]
				     "CrsCod,"				// row[7]
				     "Cod,"				// row[8]
				     "Status"				// row[9]
			      " FROM notif"
			      " WHERE ToUsrCod=%ld AND TimeNotif>=FROM_UNIXTIME(%ld)"
			      " ORDER BY TimeNotif DESC",
			      Gbl.Usrs.Me.UsrDat.UsrCod,beginTime);

   if (NumNotifications)	// Notifications found
     {
      getNotificationsOut->numNotifications = (int) NumNotifications;
      getNotificationsOut->notificationsArray.__size = (int) NumNotifications;
      getNotificationsOut->notificationsArray.__ptr = soap_malloc (Gbl.soap,(getNotificationsOut->notificationsArray.__size) * sizeof (*(getNotificationsOut->notificationsArray.__ptr)));

      for (NumNotif = 0;
	   NumNotif < NumNotifications;
	   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_GetNotifyEventFromDB ((const char *) row[1]);
         getNotificationsOut->notificationsArray.__ptr[NumNotif].eventType = (char *) soap_malloc (Gbl.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 course (row[7]) */
         Crs.CrsCod = Str_ConvertStrCodToLongCod (row[7]);
         Crs_GetDataOfCourseByCod (&Crs);

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

         if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,Crs.CrsCod))	// Get some user's data from database
           {
            getNotificationsOut->notificationsArray.__ptr[NumNotif].userNickname = (char *) soap_malloc (Gbl.soap,Nck_MAX_BYTES_NICKNAME_WITHOUT_ARROBA + 1);
            Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userNickname,
                      Gbl.Usrs.Other.UsrDat.Nickname,
                      Nck_MAX_BYTES_NICKNAME_WITHOUT_ARROBA);

            getNotificationsOut->notificationsArray.__ptr[NumNotif].userSurname1  = (char *) soap_malloc (Gbl.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  = (char *) soap_malloc (Gbl.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 = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME + 1);
            Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userFirstname,
                      Gbl.Usrs.Other.UsrDat.FirstName,
                      Usr_MAX_BYTES_FIRSTNAME_OR_SURNAME);

            Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
            getNotificationsOut->notificationsArray.__ptr[NumNotif].userPhoto = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_WWW + 1);
            Str_Copy (getNotificationsOut->notificationsArray.__ptr[NumNotif].userPhoto,
                      PhotoURL,
                      Cns_MAX_BYTES_WWW);
           }
         else
           {
	    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;
           }

         /* Get institution (row[4]) */
         Ins.InsCod = Str_ConvertStrCodToLongCod (row[4]);
         Ins_GetDataOfInstitutionByCod (&Ins,Ins_GET_BASIC_DATA);

         /* Get centre (row[5]) */
         Ctr.CtrCod = Str_ConvertStrCodToLongCod (row[5]);
         Ctr_GetDataOfCentreByCod (&Ctr);

         /* Get degree (row[6]) */
         Deg.DegCod = Str_ConvertStrCodToLongCod (row[6]);
         Deg_GetDataOfDegreeByCod (&Deg);

         /* 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 = (char *) soap_malloc (Gbl.soap,Ntf_MAX_BYTES_NOTIFY_LOCATION + 1);

	 if (NotifyEvent == Ntf_EVENT_FORUM_POST_COURSE ||
	     NotifyEvent == Ntf_EVENT_FORUM_REPLY)
	   {
	    For_GetForumTypeAndLocationOfAPost (Cod,&ForumSelected);
            For_SetForumName (&ForumSelected,
        	              ForumName,Gbl.Prefs.Language,false);	// Set forum name in recipient's language
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"%s: %s",
                     Txt_Forum,ForumName);
           }
         else if (Crs.CrsCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"%s: %s",
                     Txt_Course,Crs.ShrtName);
         else if (Deg.DegCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"%s: %s",
                     Txt_Degree,Deg.ShrtName);
         else if (Ctr.CtrCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"%s: %s",
                     Txt_Centre,Ctr.ShrtName);
         else if (Ins.InsCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumNotif].location,"%s: %s",
                     Txt_Institution,Ins.ShrtName);
         else
            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,Crs.CrsCod,Gbl.Usrs.Me.UsrDat.UsrCod,
                                        true);

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

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

            /* Free memory used by content string */
            free ((void *) 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 (void)
  {
   extern const char *Lan_STR_LANG_ID[1 + Lan_NUM_LANGUAGES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   Lan_Language_t Lan;

   /***** Get user's language *****/
   if (DB_QuerySELECT (&mysql_res,"can not get user's language",
		       "SELECT Language FROM usr_data"
		       " WHERE UsrCod=%ld",
		       Gbl.Usrs.Me.UsrDat.UsrCod) != 1)
      return soap_receiver_fault (Gbl.soap,
	                          "Can not get user's language from database",
	                          "User doen't exist in database");

   /***** Get language from database *****/
   row = mysql_fetch_row (mysql_res);

   /* Get language (row[0]) */
   Gbl.Prefs.Language = Lan_LANGUAGE_UNKNOWN;	// Language unknown
   for (Lan = (Lan_Language_t) 1;
	Lan <= Lan_NUM_LANGUAGES;
	Lan++)
      if (!strcasecmp (row[0],Lan_STR_LANG_ID[Lan]))
        {
	 Gbl.Prefs.Language = Lan;
         break;
        }
   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);

   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[1 + 10 + 1];
   unsigned NumNtf;
   unsigned NumNtfsMarkedAsRead = 0;
   long NtfCod;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_markNotificationsAsRead;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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))
      return soap_receiver_fault (Gbl.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.Role;

   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,1 + 10);
         if ((NtfCod = Str_ConvertStrCodToLongCod (LongStr)) > 0)
           {
	    /***** Mark notification as read in the database *****/
	    DB_QueryUPDATE ("can not mark notification as read",
			    "UPDATE notif SET Status=(Status | %u)"
			    " WHERE NtfCod=%ld AND ToUsrCod=%ld",
			    (unsigned) Ntf_STATUS_BIT_READ,
			    (long) NtfCod,Gbl.Usrs.Me.UsrDat.UsrCod);

	    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;
   char Nickname[Nck_MAX_BYTES_NICKNAME_FROM_FORM + 1];
   char *Query = NULL;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow;
   unsigned NumRows;
   bool FirstNickname = true;
   bool ThereAreNicknames = false;
   const char *Ptr;
   bool ItsMe;
   bool NotifyByEmail;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_sendMessage;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check if the message is a reply to a previous message *****/
   if (messageCode)
     {
      /***** Check if the original message was really received by me *****/
      if (!DB_QuerySELECT (&mysql_res,"can not check original message",
			   "SELECT SUM(N) FROM"
			   " (SELECT COUNT(*) AS N FROM msg_rcv"
			   " WHERE UsrCod=%ld AND MsgCod=%ld"
			   " UNION"
			   " SELECT COUNT(*) AS N FROM msg_rcv_deleted"
			   " WHERE UsrCod=%ld AND MsgCod=%ld) AS T",
			   Gbl.Usrs.Me.UsrDat.UsrCod,(long) messageCode,
			   Gbl.Usrs.Me.UsrDat.UsrCod,(long) messageCode))
         return soap_sender_fault (Gbl.soap,
                                   "Can not check original message",
                                   "Error reading from database");

      /***** Get number of rows *****/
      row = mysql_fetch_row (mysql_res);
      if (sscanf (row[0],"%u",&NumRows) != 1)
         return soap_sender_fault (Gbl.soap,
                                   "Can not check original message",
                                   "Error reading from database");

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

      if (!NumRows)
         return soap_sender_fault (Gbl.soap,
                                   "Can not send reply message",
                                   "Original message does not exist");

      /***** Get the recipient of the message *****/
      NumRows =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not check original message",
				 "SELECT UsrCod FROM msg_snt"
				 " WHERE MsgCod=%ld"
				 " UNION "
				 "SELECT UsrCod FROM msg_snt_deleted"
				 " WHERE MsgCod=%ld",
				 (long) messageCode,(long) messageCode);
      if (NumRows)	// Message found in any of the two tables of sent messages
        {
         row = mysql_fetch_row (mysql_res);
         ReplyUsrCod = Str_ConvertStrCodToLongCod (row[0]);
        }

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

      if (!NumRows)
         return soap_sender_fault (Gbl.soap,
                                   "Can not send reply message",
                                   "Original message does not exist");
     }

   /***** Allocate space for query *****/
   if ((Query = (char *) malloc (API_MAX_BYTES_QUERY_RECIPIENTS + 1)) == NULL)
      Lay_NotEnoughMemoryExit ();

   /***** Build query for recipients from database *****/
   if (ReplyUsrCod > 0)
      snprintf (Query,API_MAX_BYTES_QUERY_RECIPIENTS + 1,
	        "SELECT UsrCod FROM usr_data"
	        " WHERE UsrCod=%ld",
	        ReplyUsrCod);
   else
      Query[0] = '\0';

   /***** Loop over recipients' nicknames building query *****/
   Ptr = to;
   while (*Ptr)
     {
      /* Find next string in text until comma (leading and trailing spaces are removed) */
      Str_GetNextStringUntilComma (&Ptr,Nickname,Nck_MAX_BYTES_NICKNAME_FROM_FORM);

      /* Check if string is a valid nickname */
      if (Nck_CheckIfNickWithArrobaIsValid (Nickname))	// String is a nickname?
	{
         Str_RemoveLeadingArrobas (Nickname);

	 /* Check for overflow in query */
	 if (strlen (Query) + Nck_MAX_BYTES_NICKNAME_WITHOUT_ARROBA + 32 >
	     API_MAX_BYTES_QUERY_RECIPIENTS)
	    return soap_sender_fault (Gbl.soap,
				      "Can not send message",
				      "Too many recipients");

	 /* Add this nickname to query */
	 if (FirstNickname)
	   {
	    if (ReplyUsrCod > 0)
	       Str_Concat (Query," UNION ",
	                   API_MAX_BYTES_QUERY_RECIPIENTS);
	    Str_Concat (Query,"SELECT UsrCod FROM usr_nicknames"
			      " WHERE Nickname IN ('",
			API_MAX_BYTES_QUERY_RECIPIENTS);
	    FirstNickname = false;
	    ThereAreNicknames = true;
	   }
	 else
	    Str_Concat (Query,",'",
	                API_MAX_BYTES_QUERY_RECIPIENTS);
	 Str_Concat (Query,Nickname,
	             API_MAX_BYTES_QUERY_RECIPIENTS);
	 Str_Concat (Query,"'",
	             API_MAX_BYTES_QUERY_RECIPIENTS);
	}
     }
   if (ThereAreNicknames)
      Str_Concat (Query,")",
                  API_MAX_BYTES_QUERY_RECIPIENTS);

   /***** Initialize output structure *****/
   sendMessageOut->numUsers = 0;
   sendMessageOut->usersArray.__size = 0;
   sendMessageOut->usersArray.__ptr = NULL;

   if (ReplyUsrCod > 0 || ThereAreNicknames)	// There are a recipient to reply or nicknames in "to"
     {
      /***** Get users *****/
      NumRows = DB_QuerySELECT (&mysql_res,"can not get users",
				"%s",
				Query);

      sendMessageOut->numUsers = (int) NumRows;
      sendMessageOut->usersArray.__size = (int) NumRows;

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

         for (NumRow = 0;
              NumRow < NumRows;
              NumRow++)
           {
            /* 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))
        	 {
                  /* This received message must be notified by email? */
        	  ItsMe = Usr_ItsMe (Gbl.Usrs.Other.UsrDat.UsrCod);
                  NotifyByEmail = (!ItsMe &&
                                   (Gbl.Usrs.Other.UsrDat.NtfEvents.SendEmail & (1 << Ntf_EVENT_MESSAGE)));

                  /* Send message to this user */
                  if ((ReturnCode = API_SendMessageToUsr ((long) messageCode,Gbl.Usrs.Me.UsrDat.UsrCod,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 (&(sendMessageOut->usersArray.__ptr[NumRow]),&Gbl.Usrs.Other.UsrDat,false);
        	 }
           }
        }

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

/*****************************************************************************/
/************************* Send a message to one user ************************/
/*****************************************************************************/
/*
API_SendMessageToUsr ((long) messageCode,
                      Gbl.Usrs.Me.UsrDat.UsrCod,ReplyUsrCod,Gbl.Usrs.Other.UsrDat.UsrCod,
                      NotifyByEmail,subject,body)) != SOAP_OK)
*/
static int API_SendMessageToUsr (long OriginalMsgCod,
                                 long SenderUsrCod,long ReplyUsrCod,long RecipientUsrCod,
                                 bool NotifyByEmail,
                                 const char *Subject,const char *Content)
  {
   static bool MsgAlreadyInserted = false;
   static long NewMsgCod;

   /***** 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 */
      NewMsgCod =
      DB_QueryINSERTandReturnCode ("can not create message",
				   "INSERT INTO msg_content"
				   " (Subject,Content,MedCod)"
				   " VALUES"
				   " ('%s','%s',-1)",
				   Subject,Content);

      /* Insert message in sent messages */
      DB_QueryINSERT ("can not create message",
		      "INSERT INTO msg_snt"
	              " (MsgCod,CrsCod,UsrCod,Expanded,CreatTime)"
                      " VALUES"
                      " (%ld,-1,%ld,'N',NOW())",
		      NewMsgCod,SenderUsrCod);

      MsgAlreadyInserted = true;
     }

   /***** Insert message received in the database *****/
   DB_QueryINSERT ("can not create received message",
		   "INSERT INTO msg_rcv"
	           " (MsgCod,UsrCod,Notified,Open,Replied,Expanded)"
                   " VALUES"
                   " (%ld,%ld,'%c','N','N','N')",
		   NewMsgCod,RecipientUsrCod,
		   NotifyByEmail ? 'Y' :
				   'N');

   /***** Create notification for this recipient.
          If this recipient wants to receive notifications by email, activate the sending of a notification *****/
   DB_QueryINSERT ("can not create new notification event",
		   "INSERT INTO notif"
	           " (NotifyEvent,ToUsrCod,FromUsrCod,InsCod,DegCod,CrsCod,Cod,TimeNotif,Status)"
                   " VALUES"
                   " (%u,%ld,%ld,-1,-1,-1,%ld,NOW(),%u)",
		   (unsigned) Ntf_EVENT_MESSAGE,
		   RecipientUsrCod,
		   SenderUsrCod,
		   NewMsgCod,
		   (unsigned) (NotifyByEmail ? Ntf_STATUS_BIT_EMAIL :
					       0));

   /***** If this recipient is the original sender of a message been replied... *****/
   if (RecipientUsrCod == ReplyUsrCod)
      /***** ...then update received message setting Replied field to true *****/
      DB_QueryUPDATE ("can not update a received message",
		      "UPDATE msg_rcv SET Replied='Y'"
	              " WHERE MsgCod=%ld AND UsrCod=%ld",
		      OriginalMsgCod,SenderUsrCod);

   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 *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_sendNotice;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (Gbl.Hierarchy.Crs.CrsCod,-1L)) != SOAP_OK)
      return ReturnCode;

   /***** Get degree of current course *****/
   if ((ReturnCode = API_GetCurrentDegCodFromCurrentCrsCod ()) != SOAP_OK)
     return ReturnCode;

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

   /***** Insert notice in the database *****/
   /* Get the code of the inserted item */
   NotCod =
   DB_QueryINSERTandReturnCode ("can not create message",
				"INSERT INTO notices"
				" (CrsCod,UsrCod,CreatTime,Content,Status)"
				" VALUES"
				" (%ld,%ld,NOW(),'%s',%u)",
				Gbl.Hierarchy.Crs.CrsCod,Gbl.Usrs.Me.UsrDat.UsrCod,
				body,(unsigned) Not_ACTIVE_NOTICE);

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

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

   return SOAP_OK;
  }

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

int swad__getTestConfig (struct soap *soap,
                         char *wsKey,int courseCode,				// input
                         struct swad__getTestConfigOutput *getTestConfigOut)	// output
  {
   extern const char *Tst_FeedbackXML[Tst_NUM_TYPES_FEEDBACK];
   int ReturnCode;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getTestConfig;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Role != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_TCH)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get degree of current course *****/
   if ((ReturnCode = API_GetCurrentDegCodFromCurrentCrsCod ()) != SOAP_OK)
     return ReturnCode;

   /***** Set default result to empty *****/
   getTestConfigOut->numQuestions = getTestConfigOut->minQuestions = getTestConfigOut->defQuestions = getTestConfigOut->maxQuestions = 0;
   getTestConfigOut->feedback = (char *) soap_malloc (Gbl.soap,Tst_MAX_BYTES_FEEDBACK_TYPE + 1);
   getTestConfigOut->feedback[0] = '\0';

   /***** Get test configuration *****/
   if ((ReturnCode = API_GetTstConfig ((long) courseCode)) != SOAP_OK)
      return ReturnCode;
   getTestConfigOut->pluggable = (Gbl.Test.Config.Pluggable == Tst_PLUGGABLE_YES) ? 1 :
	                                                                            0;
   getTestConfigOut->minQuestions = (int) Gbl.Test.Config.Min;
   getTestConfigOut->defQuestions = (int) Gbl.Test.Config.Def;
   getTestConfigOut->maxQuestions = (int) Gbl.Test.Config.Max;
   Str_Copy (getTestConfigOut->feedback,
             Tst_FeedbackXML[Gbl.Test.Config.Feedback],
             Tst_MAX_BYTES_FEEDBACK_TYPE);

   /***** Get number of tests *****/
   if (Gbl.Test.Config.Pluggable == Tst_PLUGGABLE_YES &&
       Gbl.Test.Config.Max > 0)
      getTestConfigOut->numQuestions = API_GetNumTestQuestionsInCrs ((long) courseCode);

   return SOAP_OK;
  }

/*****************************************************************************/
/****** Get configuration of tests from database giving a course code ********/
/*****************************************************************************/

static int API_GetTstConfig (long CrsCod)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

   /***** Query database *****/
   if (DB_QuerySELECT (&mysql_res,"can not get test configuration",
		       "SELECT Pluggable,Min,Def,Max,MinTimeNxtTstPerQst,Feedback"
		       " FROM tst_config WHERE CrsCod=%ld",
		       CrsCod))
     {
      /***** Get minimun, default and maximum *****/
      row = mysql_fetch_row (mysql_res);
      Tst_GetConfigFromRow (row);
     }
   else // NumRows == 0
     {
      Gbl.Test.Config.Pluggable = Tst_PLUGGABLE_UNKNOWN;
      Gbl.Test.Config.Min = Gbl.Test.Config.Def = Gbl.Test.Config.Max = 0;
      Gbl.Test.Config.Feedback = Tst_FEEDBACK_DEFAULT;
     }

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

   return SOAP_OK;
  }

/*****************************************************************************/
/** Get number of visible test questions from database giving a course code **/
/*****************************************************************************/

static int API_GetNumTestQuestionsInCrs (long CrsCod)
  {
   /***** Get number of questions *****/
   // Reject questions with any tag hidden
   // Select only questions with tags
   return
   (int) DB_QueryCOUNT ("can not get number of test questions",
			"SELECT COUNT(*)"
			" FROM tst_questions,tst_question_tags,tst_tags"
			" WHERE tst_questions.CrsCod=%ld"
			" AND tst_questions.QstCod NOT IN"

			" (SELECT tst_question_tags.QstCod"
			" FROM tst_tags,tst_question_tags"
			" WHERE tst_tags.CrsCod=%ld"
			" AND tst_tags.TagHidden='Y'"
			" AND tst_tags.TagCod=tst_question_tags.TagCod)"

			" AND tst_questions.QstCod=tst_question_tags.QstCod"
			" AND tst_question_tags.TagCod=tst_tags.TagCod"
			" AND tst_tags.CrsCod=%ld",
			CrsCod,CrsCod,CrsCod);
  }

/*****************************************************************************/
/************************* 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 *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getTests;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Role != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_TCH)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get degree of current course *****/
   if ((ReturnCode = API_GetCurrentDegCodFromCurrentCrsCod ()) != SOAP_OK)
     return ReturnCode;

   /***** 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 ((ReturnCode = API_GetTstConfig ((long) courseCode)) != SOAP_OK)
      return ReturnCode;

   if (Gbl.Test.Config.Pluggable == Tst_PLUGGABLE_YES)
     {
      /***** Get tags *****/
      if ((ReturnCode = API_GetTstTags ((long) courseCode,getTestsOut)) != SOAP_OK)
         return ReturnCode;

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

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

      /***** Get tags for each question *****/
      if ((ReturnCode = API_GetTstQuestionTags ((long) courseCode,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 (long CrsCod,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Lan_STR_LANG_ID[1 + Lan_NUM_LANGUAGES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow;
   unsigned NumRows;

   /***** Get available tags from database *****/
   NumRows = DB_QuerySELECT (&mysql_res,"can not get test tags",
			     "SELECT TagCod,TagTxt"
			     " FROM tst_tags"
			     " WHERE CrsCod=%ld AND TagHidden='N'"
			     " ORDER BY TagTxt",
			     CrsCod);

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

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

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

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

         /* Get tag text (row[1]) */
         getTestsOut->tagsArray.__ptr[NumRow].tagText = (char *) soap_malloc (Gbl.soap,Tst_MAX_BYTES_TAG + 1);
	 Str_Copy (getTestsOut->tagsArray.__ptr[NumRow].tagText,row[1],
	           Tst_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 (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow;
   unsigned NumRows;
   Tst_AnswerType_t AnswerType;

   /***** Get recent test questions from database *****/
   // DISTINCTROW is necessary to not repeat questions
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get test questions",
			      "SELECT DISTINCTROW tst_questions.QstCod,"
			      "tst_questions.AnsType,tst_questions.Shuffle,"
			      "tst_questions.Stem,tst_questions.Feedback"
			      " FROM tst_questions,tst_question_tags,tst_tags"
			      " WHERE tst_questions.CrsCod=%ld"
			      " AND tst_questions.QstCod NOT IN"
			      " (SELECT tst_question_tags.QstCod FROM tst_tags,tst_question_tags"
			      " WHERE tst_tags.CrsCod=%ld AND tst_tags.TagHidden='Y'"
			      " AND tst_tags.TagCod=tst_question_tags.TagCod)"
			      " AND tst_questions.QstCod=tst_question_tags.QstCod"
			      " AND tst_question_tags.TagCod=tst_tags.TagCod"
			      " AND tst_tags.CrsCod=%ld"
			      " AND "
			      "("
			      "tst_questions.EditTime>=FROM_UNIXTIME(%ld)"
			      " OR "
			      "tst_tags.ChangeTime>=FROM_UNIXTIME(%ld)"
			      ")"
			      " ORDER BY QstCod",
			      CrsCod,CrsCod,CrsCod,
			      BeginTime,
			      BeginTime);

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

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

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

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

         /* Get answer type (row[1]) */
         AnswerType = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[1]);
         getTestsOut->questionsArray.__ptr[NumRow].answerType = (char *) soap_malloc (Gbl.soap,Tst_MAX_BYTES_ANSWER_TYPE + 1);
	 Str_Copy (getTestsOut->questionsArray.__ptr[NumRow].answerType,
	           Tst_StrAnswerTypesXML[AnswerType],
	           Tst_MAX_BYTES_ANSWER_TYPE);

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

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

         /* Get question feedback (row[4]) */
         getTestsOut->questionsArray.__ptr[NumRow].feedback = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_TEXT + 1);
         Str_Copy (getTestsOut->questionsArray.__ptr[NumRow].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 (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   unsigned Index;

   /***** Get recent test questions from database *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get test answers",
			      "SELECT QstCod,AnsInd,Correct,Answer,Feedback"
			      " FROM tst_answers WHERE QstCod IN "
			      "(SELECT tst_questions.QstCod"
			      " FROM tst_questions,tst_question_tags,tst_tags"
			      " WHERE tst_questions.CrsCod=%ld"
			      " AND tst_questions.QstCod NOT IN"
			      " (SELECT tst_question_tags.QstCod FROM tst_tags,tst_question_tags"
			      " WHERE tst_tags.CrsCod=%ld AND tst_tags.TagHidden='Y'"
			      " AND tst_tags.TagCod=tst_question_tags.TagCod)"
			      " AND tst_questions.QstCod=tst_question_tags.QstCod"
			      " AND tst_question_tags.TagCod=tst_tags.TagCod"
			      " AND tst_tags.CrsCod=%ld"
			      " AND "
			      "("
			      "tst_questions.EditTime>=FROM_UNIXTIME(%ld)"
			      " OR "
			      "tst_tags.ChangeTime>=FROM_UNIXTIME(%ld)"
			      ")"
			      ")"
			      " ORDER BY QstCod,AnsInd",
			      CrsCod,CrsCod,CrsCod,
			      BeginTime,
			      BeginTime);

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

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

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

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

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

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

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

         /* Get feedback (row[4]) */
         getTestsOut->answersArray.__ptr[NumRow].answerFeedback = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_TEXT + 1);
         Str_Copy (getTestsOut->answersArray.__ptr[NumRow].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 (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   unsigned Index;

   /***** Get recent test questions from database *****/
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get test question tags",
			      "SELECT QstCod,TagCod,TagInd"
			      " FROM tst_question_tags WHERE QstCod IN "
			      "(SELECT tst_questions.QstCod"
			      " FROM tst_questions,tst_question_tags,tst_tags"
			      " WHERE tst_questions.CrsCod=%ld"
			      " AND tst_questions.QstCod NOT IN"
			      " (SELECT tst_question_tags.QstCod FROM tst_tags,tst_question_tags"
			      " WHERE tst_tags.CrsCod=%ld AND tst_tags.TagHidden='Y'"
			      " AND tst_tags.TagCod=tst_question_tags.TagCod)"
			      " AND tst_questions.QstCod=tst_question_tags.QstCod"
			      " AND tst_question_tags.TagCod=tst_tags.TagCod"
			      " AND tst_tags.CrsCod=%ld"
			      " AND "
			      "("
			      "tst_questions.EditTime>=FROM_UNIXTIME(%ld)"
			      " OR "
			      "tst_tags.ChangeTime>=FROM_UNIXTIME(%ld)"
			      ")"
			      ")"
			      " ORDER BY QstCod,TagInd",
			      CrsCod,CrsCod,CrsCod,
			      BeginTime,
			      BeginTime);

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

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

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

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

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

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

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

   return SOAP_OK;
  }

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

#define API_MAX_BYTES_DEGREES_STR (1024 - 1)

int swad__getTrivialQuestion (struct soap *soap,
                              char *wsKey,char *degrees,float lowerScore,float upperScore,	// input
                              struct swad__getTrivialQuestionOutput *getTrivialQuestionOut)	// output
  {
   extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
   int ReturnCode;
   const char *Ptr;
   char LongStr[1 + 10 + 1];
   char DegreesStr[API_MAX_BYTES_DEGREES_STR + 1];
   char DegStr[ 1 + 1 + 1 + 10 + 1 + 1];
   //   DegStr=",   '   - number '  \0"
   long DegCod;
   bool FirstDegree = true;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow;
   unsigned NumRows;
   long QstCod = -1L;
   Tst_AnswerType_t AnswerType;
   unsigned Index;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getTrivialQuestion;

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

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

   if (!DegreesStr[0])	// Degrees not found
      return soap_sender_fault (Gbl.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 (Gbl.soap,
	                        "Bad score interval",
	                        "lowerScore or upperScore values not valid");

   /***** Start query *****/
   Str_SetDecimalPointToUS ();	// To print the floating point as a dot
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get test questions",
			      "SELECT DISTINCTROW tst_questions.QstCod,"
			      "tst_questions.AnsType,tst_questions.Shuffle,"
			      "tst_questions.Stem,tst_questions.Feedback,"
			      "tst_questions.Score/tst_questions.NumHits AS S"
			      " FROM courses,tst_questions"
			      " WHERE courses.DegCod IN (%s)"
			      " AND courses.CrsCod=tst_questions.CrsCod"
			      " AND tst_questions.AnsType='unique_choice'"
			      " AND tst_questions.NumHits>0"
			      " AND tst_questions.QstCod NOT IN"
			      " (SELECT tst_question_tags.QstCod"
			      " FROM courses,tst_tags,tst_question_tags"
			      " WHERE courses.DegCod IN (%s)"
			      " AND courses.CrsCod=tst_tags.CrsCod"
			      " AND tst_tags.TagHidden='Y'"
			      " AND tst_tags.TagCod=tst_question_tags.TagCod)"
			      " HAVING S>='%f' AND S<='%f'"
			      " ORDER BY RAND(NOW()) LIMIT 1",
			      DegreesStr,DegreesStr,
			      lowerScore,upperScore);
   Str_SetDecimalPointToLocal ();	// Return to local system

   if (NumRows == 1)	// Question found
     {
      /* Get next 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 = Tst_ConvertFromStrAnsTypDBToAnsTyp (row[1]);
      getTrivialQuestionOut->question.answerType = (char *) soap_malloc (Gbl.soap,Tst_MAX_BYTES_ANSWER_TYPE + 1);
      Str_Copy (getTrivialQuestionOut->question.answerType,
                Tst_StrAnswerTypesXML[AnswerType],
                Tst_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 = (char *) soap_malloc (Gbl.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 = (char *) soap_malloc (Gbl.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 = (char *) soap_malloc (Gbl.soap,1);
      getTrivialQuestionOut->question.answerType[0] = '\0';

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

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

      /* Get question feedback (row[4]) */
      getTrivialQuestionOut->question.feedback = (char *) soap_malloc (Gbl.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 *****/
      NumRows =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get test answers",
				 "SELECT QstCod,AnsInd,Correct,Answer,Feedback"
				 " FROM tst_answers WHERE QstCod=%ld"
				 " ORDER BY AnsInd",
				 QstCod);

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

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

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

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

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

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

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

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

      /***** 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 NumRows;
   int NumGame;
   long GamCod;
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];
   long StartTime;
   long EndTime;
   size_t Length;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getGames;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;

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

   /***** Check if course code is correct *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return soap_sender_fault (Gbl.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.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check if I belong to the course *****/
   switch (Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role)
     {
      case Rol_STD:
      case Rol_TCH:
	 // OK. I belong to the course
	 break;
      default:
	 return soap_receiver_fault (Gbl.soap,
				     "Request forbidden",
				     "Requester must belong to the course");
     }

   /***** Query list of games *****/
   /*
   mysql> DESCRIBE games;
   +-----------+-------------------------------------------+------+-----+---------+----------------+
   | Field     | Type                                      | Null | Key | Default | Extra          |
   +-----------+-------------------------------------------+------+-----+---------+----------------+
   | GamCod    | int(11)                                   | NO   | PRI | NULL    | auto_increment |
   | Scope     | enum('Sys','Cty','Ins','Ctr','Deg','Crs') | NO   | MUL | Sys     |                |
   | Cod       | int(11)                                   | NO   |     | -1      |                |
   | Hidden    | enum('N','Y')                             | NO   |     | N       |                |
   | NumNotif  | int(11)                                   | NO   |     | 0       |                |
   | Roles     | int(11)                                   | NO   |     | 0       |                |
   | UsrCod    | int(11)                                   | NO   |     | NULL    |                |
   | StartTime | datetime                                  | NO   |     | NULL    |                |
   | EndTime   | datetime                                  | NO   |     | NULL    |                |
   | Title     | varchar(2047)                             | NO   |     | NULL    |                |
   | Txt       | text                                      | NO   |     | NULL    |                |
   +-----------+-------------------------------------------+------+-----+---------+----------------+
   11 rows in set (0.00 sec)
   */
   NumRows =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get games",
			      "SELECT GamCod,"				// row[0]
			             "UsrCod,"				// row[1]
			             "UNIX_TIMESTAMP(StartTime) AS ST,"	// row[2]
			             "UNIX_TIMESTAMP(EndTime) AS ET,"	// row[3]
			             "Title,"				// row[4]
			             "Txt"				// row[5]
			      " FROM att_events"
			      " WHERE Scope='%s' AND Cod=%ld"
			      " AND Hidden='N'"
			      " AND NOW() BETWEEN StartTime AND EndTime"
			      " AND (GamCod NOT IN (SELECT GamCod FROM gam_grp) OR"
			      " GamCod IN (SELECT gam_grp.GamCod FROM gam_grp,crs_grp_usr"
			      " WHERE crs_grp_usr.UsrCod=%ld"
			      " AND gam_grp.GrpCod=crs_grp_usr.GrpCod))"
			      " ORDER BY ST DESC,ET DESC,Title DESC",
			      Sco_GetDBStrFromScope (Hie_CRS),courseCode,
			      Gbl.Usrs.Me.UsrDat.UsrCod);

   getGamesOut->gamesArray.__size =
   getGamesOut->numGames          = (int) NumRows;

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

      for (NumGame = 0;
	   NumGame < getGamesOut->numGames;
	   NumGame++)
	{
	 /* Get next group */
	 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]);
         if (API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,Gbl.Hierarchy.Crs.CrsCod))	// Get some user's data from database
           {
            Length = strlen (Gbl.Usrs.Other.UsrDat.Surname1);
            getGamesOut->gamesArray.__ptr[NumGame].userSurname1 = (char *) soap_malloc (Gbl.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 = (char *) soap_malloc (Gbl.soap,Length + 1);
            Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].userSurname2,
                      Gbl.Usrs.Other.UsrDat.Surname2,
                      Length);

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

            Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
            Length = strlen (PhotoURL);
            getGamesOut->gamesArray.__ptr[NumGame].userPhoto = (char *) soap_malloc (Gbl.soap,Length + 1);
            Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].userPhoto,
                      PhotoURL,
                      Length);
           }
         else
           {
            getGamesOut->gamesArray.__ptr[NumGame].userSurname1  = NULL;
            getGamesOut->gamesArray.__ptr[NumGame].userSurname2  = NULL;
            getGamesOut->gamesArray.__ptr[NumGame].userFirstname = NULL;
            getGamesOut->gamesArray.__ptr[NumGame].userPhoto     = NULL;
           }

	 /* 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 title of the game (row[4]) */
         Length = strlen (row[4]);
         getGamesOut->gamesArray.__ptr[NumGame].title = (char *) soap_malloc (Gbl.soap,Length + 1);
         Str_Copy (getGamesOut->gamesArray.__ptr[NumGame].title,row[4],
                   Length);

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

	 /* Get list of groups for this game */
         API_GetListGrpsInGameFromDB (GamCod,&(getGamesOut->gamesArray.__ptr[NumGame].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_GetListGrpsInGameFromDB (long GamCod,char **ListGroups)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   long NumGrps;
   long NumGrp;
   long GrpCod;
   char GrpCodStr[10 + 1];
   size_t Length;

   /***** Get list of groups *****/
   NumGrps =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get groups of a game",
			      "SELECT GrpCod FROM gam_grp WHERE GamCod=%ld",
			      GamCod);
   if (NumGrps == 0)
      *ListGroups = NULL;
   else	// Events found
     {
      Length = NumGrps * (10 + 1) - 1;
      *ListGroups = soap_malloc (Gbl.soap,Length + 1);
      (*ListGroups)[0] = '\0';

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

	 /* Get group code (row[0]) */
	 GrpCod = Str_ConvertStrCodToLongCod (row[0]);
	 snprintf (GrpCodStr,sizeof (GrpCodStr),
		   NumGrp ? ",%ld" :
			    "%ld",
		   GrpCod);
	 Str_Concat (*ListGroups,GrpCodStr,
	             Length);
	}
     }

   /***** 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];
   unsigned long FileSize;
   unsigned long NumBytesRead;

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getDirectoryTree;
   Gbl.Hierarchy.Crs.CrsCod = (long) courseCode;
   Gbl.Crs.Grps.GrpCod = (long) groupCode;

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

   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (Gbl.Hierarchy.Crs.CrsCod,Gbl.Crs.Grps.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.Role != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_TCH)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Check if I belong to course/group *****/
   if (Gbl.Crs.Grps.GrpCod > 0)
      if (!Grp_GetIfIBelongToGrp (Gbl.Crs.Grps.GrpCod))
	 return soap_receiver_fault (Gbl.soap,
				     "Request forbidden",
				     "Requester must belong to group");

   /***** Check if tree code is correct *****/
   if (treeCode <= 0 || treeCode > 3)
      return soap_sender_fault (Gbl.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 (Gbl.soap,
	                        "Bad course code",
	                        "Course code must be a integer greater than 0");

   /* Initialize path to private directory */
   Gbl.Hierarchy.Crs.CrsCod = (courseCode > 0) ? (long) courseCode :
	                                          -1L;
   Gbl.Crs.Grps.GrpCod = (groupCode > 0) ? (long) groupCode :
	                                          -1L;

   snprintf (Gbl.Crs.PathPriv,sizeof (Gbl.Crs.PathPriv),
	     "%s/%ld",
             Cfg_PATH_CRS_PRIVATE,Gbl.Hierarchy.Crs.CrsCod);
   Brw_InitializeFileBrowser ();
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Path,Brw_RootFolderInternalNames[Gbl.FileBrowser.Type],
	     PATH_MAX);
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Name,".",
	     NAME_MAX);
   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,Gbl.UniqueNameEncrypted);

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

   /* Get directory tree into XML file */
   XML_WriteStartFile (Gbl.F.XML,"tree",false);
   if (!Brw_CheckIfFileOrFolderIsSetAsHiddenInDB (Brw_IS_FOLDER,
                                                  Gbl.FileBrowser.FilFolLnk.Full)) // If root folder is visible
      API_ListDir (1,Gbl.FileBrowser.Priv.PathRootFolder,Brw_RootFolderInternalNames[Gbl.FileBrowser.Type]);
   XML_WriteEndFile (Gbl.F.XML,"tree");

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

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

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

   return SOAP_OK;
  }

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

static void API_ListDir (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;

   /***** 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,
	 	     PATH_MAX);
	    Str_Copy (Gbl.FileBrowser.FilFolLnk.Name,FileList[NumFile]->d_name,
	 	     NAME_MAX);

	    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 (Level,Brw_IS_FOLDER,FileList[NumFile]->d_name))
		    {
		     /* List subtree starting at this this directory */
		     API_ListDir (Level + 1,PathFileRel,PathFileInExplTree);

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

	 free ((void *) FileList[NumFile]);
	}
      free ((void *) 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
// If the row is visible, return true. If it is hidden, return false

static bool API_WriteRowFileBrowser (unsigned Level,Brw_FileType_t FileType,const char *FileName)
  {
   extern const char *Txt_NEW_LINE;
   extern const char *Txt_LICENSES[Brw_NUM_LICENSES];
   struct FileMetadata FileMetadata;
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];

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

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

   /* Write file or folder data */
   if (FileType == Brw_IS_FOLDER)
      fprintf (Gbl.F.XML,"<dir name=\"%s\">%s",
	       FileName,Txt_NEW_LINE);
   else	// File or link
     {
      /* Get file metadata */
      Brw_GetFileMetadataByPath (&FileMetadata);
      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_AddPathToDB (-1L,FileMetadata.FilFolLnk.Type,
	                                        Gbl.FileBrowser.FilFolLnk.Full,false,Brw_LICENSE_DEFAULT);

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

      fprintf (Gbl.F.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 true;
  }

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

static void API_IndentXMLLine (unsigned Level)
  {
   for ( ;
	Level;
	Level--)
      fprintf (Gbl.F.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 FileMetadata FileMetadata;
   char URL[Cns_MAX_BYTES_WWW + 1];
   char PhotoURL[Cns_MAX_BYTES_WWW + 1];

   /***** Initializations *****/
   Gbl.soap = soap;
   Gbl.WebService.Function = API_getFile;

   /***** Allocate space for strings *****/
   getFileOut->fileName        = (char *) soap_malloc (Gbl.soap,NAME_MAX + 1);
   getFileOut->URL             = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_WWW + 1);
   getFileOut->license         = (char *) soap_malloc (Gbl.soap,Brw_MAX_BYTES_LICENSE + 1);
   getFileOut->publisherName   = (char *) soap_malloc (Gbl.soap,Usr_MAX_BYTES_FULL_NAME + 1);
   getFileOut->publisherPhoto  = (char *) soap_malloc (Gbl.soap,Cns_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_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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 (Gbl.soap,
	                        "Bad file code",
	                        "File code must be a integer greater than 0");

   Brw_GetFileMetadataByCod (&FileMetadata);
   if (FileMetadata.FilCod <= 0)
      return soap_sender_fault (Gbl.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.Ins.InsCod,
                                  &Gbl.Hierarchy.Ctr.CtrCod,
                                  &Gbl.Hierarchy.Deg.DegCod,
                                  &Gbl.Hierarchy.Crs.CrsCod,
                                  &Gbl.Crs.Grps.GrpCod);

   /***** Get some of my data *****/
   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (Gbl.Hierarchy.Crs.CrsCod,Gbl.Crs.Grps.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.Role != Rol_STD &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_NET &&
       Gbl.Usrs.Me.UsrDat.Roles.InCurrentCrs.Role != Rol_TCH)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Check if I belong to group *****/
   if (Gbl.Crs.Grps.GrpCod > 0)
      if (!Grp_GetIfIBelongToGrp (Gbl.Crs.Grps.GrpCod))
	 return soap_receiver_fault (Gbl.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.Role != Rol_TCH)
	    return soap_receiver_fault (Gbl.soap,
					"Wrong tree",
					"Wrong file zone");
	 break;
      default:
         return soap_sender_fault (Gbl.soap,
                                   "Wrong tree",
                                   "Wrong file zone");
     }

   /***** Set paths *****/
   Hie_InitHierarchy ();
   Brw_InitializeFileBrowser ();
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Path,FileMetadata.FilFolLnk.Path,
	     PATH_MAX);
   Str_Copy (Gbl.FileBrowser.FilFolLnk.Name,FileMetadata.FilFolLnk.Name,
	     NAME_MAX);
   Brw_SetFullPathInTree ();

   /***** Get file size and date *****/
   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,
             Cns_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))
	 /* Copy publisher's data into output structure */
	{
	 Str_Copy (getFileOut->publisherName,Gbl.Usrs.Other.UsrDat.FullName,
	           Usr_MAX_BYTES_FULL_NAME);

	 Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL);
	 Str_Copy (getFileOut->publisherPhoto,PhotoURL,
	           Cns_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 FileMetadata FileMetadata;
   char SummaryStr[Ntf_MAX_BYTES_SUMMARY + 1];	// Not used
   char *ContentStr;
   size_t Length;

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

   /***** Check web service key *****/
   if ((ReturnCode = API_CheckWSKey (wsKey)) != SOAP_OK)
      return ReturnCode;
   if (Gbl.Usrs.Me.UsrDat.UsrCod < 0)	// Web service key does not exist in database
      return soap_receiver_fault (Gbl.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.IsHidden ||
       (FileMetadata.FileBrowser != Brw_ADMI_MRK_CRS &&
	FileMetadata.FileBrowser != Brw_ADMI_MRK_GRP))
      return soap_receiver_fault (Gbl.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.Ins.InsCod,
                                  &Gbl.Hierarchy.Ctr.CtrCod,
                                  &Gbl.Hierarchy.Deg.DegCod,
                                  &Gbl.Hierarchy.Crs.CrsCod,
                                  &Gbl.Crs.Grps.GrpCod);

   /***** Check course and group codes *****/
   if ((ReturnCode = API_CheckCourseAndGroupCodes (Gbl.Hierarchy.Crs.CrsCod,Gbl.Crs.Grps.GrpCod)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if (!API_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.Hierarchy.Crs.CrsCod))
      return soap_receiver_fault (Gbl.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.Role;

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

   /***** Check if I belong to group *****/
   if (Gbl.Crs.Grps.GrpCod > 0)
      if (!Grp_GetIfIBelongToGrp (Gbl.Crs.Grps.GrpCod))
	 return soap_receiver_fault (Gbl.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,true);
   if (ContentStr != NULL)
     {
      Length = strlen (ContentStr);
      getMarksOut->content = (char *) soap_malloc (Gbl.soap,Length + 1);
      Str_Copy (getMarksOut->content,ContentStr,
                Length);
      free ((void *) ContentStr);
      ContentStr = NULL;
     }
   else
     {
      getMarksOut->content = (char *) soap_malloc (Gbl.soap,1);
      getMarksOut->content[0] = '\0';
     }

   return SOAP_OK;
  }
