// swad_web_service.c: SWAD web service 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-2015 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 = swad.o swad_action.o swad_assignment.o swad_file_browser.o swad_chat.o swad_connected.o swad_course.o swad_cryptography.o swad_centre.o swad_date.o swad_database.o swad_degree.o swad_department.o swad_exam.o swad_file.o swad_forum.o swad_group.o swad_holiday.o swad_import.o swad_institution.o swad_layout.o swad_link.o swad_changelog.o swad_message.o swad_notice.o swad_notification.o swad_pagination.o swad_parameter.o swad_place.o swad_plugin.o swad_preference.o swad_record.o swad_session.o swad_statistic.o swad_string.o swad_syllabus.o swad_theme.o swad_test.o swad_timetable.o swad_text.o swad_user.o swad_web_service.o sha2.o
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 dirent
#include <linux/limits.h>	// For PATH_MAX
#include <linux/stddef.h>	// For NULL
#include <locale.h>		// For setlocale, LC_NUMERIC...
#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_database.h"
#include "swad_file_browser.h"
#include "swad_global.h"
#include "swad_ID.h"
#include "swad_notice.h"
#include "swad_notification.h"
#include "swad_password.h"
#include "swad_user.h"
#include "swad_web_service.h"

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

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

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

// Add new functions at the end
static const char *Svc_Functions[1+Svc_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
  };

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

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

// static int Svc_CheckIfIPIsAllowed (void);
static int Svc_GetPlgCodFromAppKey (const char *appKey);
static int Svc_CheckIdSession (const char *IdSession);
static int Svc_CheckWSKey (char *WSKey);

static int Svc_CheckCourseAndGroupCodes (long CrsCod,long GrpCod);
static int Svc_GenerateNewWSKey (long UsrCod,char *WSKey);
static int Svc_RemoveOldWSKeys (void);
static int Svc_GetCurrentDegCodFromCurrentCrsCod (void);
static int Svc_GetSomeUsrDataFromUsrCod (struct UsrData *UsrDat,long CrsCod);
static int Svc_GetRoleFromInternalRole (Rol_Role_t Role);

static int Svc_CheckParamsNewAccount (char *NewNicknameWithArroba,	// Input
                                      char *NewNicknameWithoutArroba,	// Output
                                      char *NewEmail,			// Input-output
                                      char *NewPlainPassword,		// Input
                                      char *NewEncryptedPassword);	// Output

static void Svc_CopyUsrData (struct swad__user *Usr,struct UsrData *UsrDat,bool UsrIDIsVisible);

static void Svc_GetListGrpsInAttendanceEvent (long AttCod,char **ListGroups);

static int Svc_GetLanguageFromUsrCod (long UsrCod);

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

static int Svc_GetTstConfig (long CrsCod);
static int Svc_GetNumTestQuestionsInCrs (long CrsCod);
static int Svc_GetTstTags (long CrsCod,struct swad__getTestsOutput *getTestsOut);
static int Svc_GetTstQuestions (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut);
static int Svc_GetTstAnswers (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut);
static int Svc_GetTstQuestionTags (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut);

static void Svc_ListDir (unsigned Level,const char *Path,const char *PathInTree);
static bool Svc_WriteRowFileBrowser (unsigned Level,Brw_FileType_t FileType,const char *FileName);
static void Svc_IndentXMLLine (unsigned Level);

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

void Svc_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 Svc_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 IP of the requester of a web service ***************/
/*********** is one of the IP allowed in the plugins           ***************/
/*****************************************************************************/
/*
static int Svc_CheckIfIPIsAllowed (void)
  {
   char IP[Cns_MAX_LENGTH_IP+1];
   char Query[256+Cns_MAX_LENGTH_IP];

   ***** Get IP *****
   if (getenv ("REMOTE_ADDR"))
     {
      strncpy (IP,getenv ("REMOTE_ADDR"),Cns_MAX_LENGTH_IP);
      IP[Cns_MAX_LENGTH_IP] = '\0';
     }
   else
      IP[0] = '\0';

   ***** Get number of plugins with a IP address *****
   sprintf (Query,"SELECT COUNT(*) FROM plugins WHERE IP='%s'",IP);
   if (!DB_QueryCOUNT (Query,"can not check IP"))
     {
      sprintf (Gbl.Message,"Sender's IP (%s) is forbidden",IP);
      return soap_sender_fault (Gbl.soap,
                                Gbl.Message,
                                "The IP of the requester of a web service is not allowed");
     }

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

static int Svc_GetPlgCodFromAppKey (const char *appKey)
  {
   char Query[256+Plg_MAX_LENGTH_PLUGIN_APP_KEY];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

   Gbl.WebService.PlgCod = -1L;

   /***** Get number of plugins with a IP address *****/
   sprintf (Query,"SELECT PlgCod FROM plugins WHERE AppKey='%s'",appKey);
   if (DB_QuerySELECT (Query,&mysql_res,"can not check application key"))	// 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 *Svc_GetFunctionNameFromFunCod (long FunCod)
  {
   if (FunCod < 0 || FunCod > Svc_NUM_FUNCTIONS)
      return Svc_Functions[0];
   return Svc_Functions[FunCod];
  }

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

static int Svc_CheckIdSession (const char *IdSession)
  {
   const char *Ptr;
   unsigned i;
   char Query[512];

   /***** 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) != Ses_LENGTH_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 *****/
   sprintf (Query,"SELECT COUNT(*) FROM sessions WHERE SessionId='%s'",
            IdSession);
   if (DB_QueryCOUNT (Query,"can not get session data") != 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 Svc_CheckWSKey (char *WSKey)
  {
   char Query[512];
   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 *****/
   sprintf (Query,"SELECT UsrCod,PlgCod FROM ws_keys WHERE WSKey='%s'",
            WSKey);
   if (DB_QuerySELECT (Query,&mysql_res,"can not get existence of key"))	// 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 Svc_CheckCourseAndGroupCodes (long CrsCod,long GrpCod)
  {
   char Query[512];

   /***** 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 *****/
   sprintf (Query,"SELECT COUNT(*) FROM courses WHERE CrsCod='%ld'",
            CrsCod);
   if (DB_QueryCOUNT (Query,"can not get course") != 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 *****/
      sprintf (Query,"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);
      if (DB_QueryCOUNT (Query,"can not get group") != 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 Svc_GenerateNewWSKey (long UsrCod,char *WSKey)
  {
   int ReturnCode;
   char Query[512];

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

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

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

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

   return SOAP_OK;
  }

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

static int Svc_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_Svc_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 Svc_GetCurrentDegCodFromCurrentCrsCod (void)
  {
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

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

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

      Gbl.CurrentDeg.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 *************/
/*****************************************************************************/
// UsrDat->UsrCod must contain an existing user's code

static int Svc_GetSomeUsrDataFromUsrCod (struct UsrData *UsrDat,long CrsCod)
  {
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

   /***** Get some user's data *****/
   /* Query database */
   sprintf (Query,"SELECT Surname1,Surname2,FirstName,Photo,DATE_FORMAT(Birthday,'%%Y%%m%%d')"
                  " FROM usr_data WHERE UsrCod='%ld'",
            UsrDat->UsrCod);

   /* Check number of rows in result */
   if (DB_QuerySELECT (Query,&mysql_res,"can not get user's data") != 1)
      return soap_receiver_fault (Gbl.soap,
	                          "Can not get user's data from database",
	                          "User does not exist in database");

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

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

   UsrDat->Surname1 [sizeof (UsrDat->Surname1 )-1] = '\0';
   UsrDat->Surname2 [sizeof (UsrDat->Surname2 )-1] = '\0';
   UsrDat->FirstName[sizeof (UsrDat->FirstName)-1] = '\0';

   /* Get user's photo */
   strncpy (UsrDat->Photo,row[3],sizeof (UsrDat->Photo)-1);
   UsrDat->Photo[sizeof (UsrDat->Photo)-1] = '\0';

   /* Get user's brithday */
   strncpy (UsrDat->Birthday.YYYYMMDD,row[4],4+2+2);
   UsrDat->Birthday.YYYYMMDD[4+2+2] = '\0';

   /* 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)
      sprintf (Query,"SELECT Role FROM crs_usr"
                     " WHERE CrsCod='%ld' AND UsrCod='%ld'",
               CrsCod,UsrDat->UsrCod);
   else
      sprintf (Query,"SELECT MAX(Role)"
                     " FROM crs_usr WHERE UsrCod='%ld'",
               UsrDat->UsrCod);
   if (DB_QuerySELECT (Query,&mysql_res,"can not get user's role") == 1)
     {
      /* Read the maximum role */
      row = mysql_fetch_row (mysql_res);
      if (sscanf (row[0],"%u",&UsrDat->RoleInCurrentCrsDB) != 1)
         UsrDat->RoleInCurrentCrsDB = Rol_UNKNOWN;
     }
   else
      UsrDat->RoleInCurrentCrsDB = Rol__GUEST_;

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

   return SOAP_OK;
  }

/*****************************************************************************/
/****** Get the role returned via web service from internal role code ********/
/*****************************************************************************/

static int Svc_GetRoleFromInternalRole (Rol_Role_t Role)
  {
   switch (Role)
     {
      case Rol__GUEST_:
      case Rol_VISITOR:
	 return 1;	// guest or visitor
      case Rol_STUDENT:
	 return 2;	// student
      case Rol_TEACHER:
	 return 3;	// teacher
      default:
	 return 0;	// unknown
     }
   return 0;		// unknown
  }

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

#define Svc_CHECK_NEW_ACCOUNT_OK					 0
#define Svc_CHECK_NEW_ACCOUNT_NICKNAME_NOT_VALID			-1
#define Svc_CHECK_NEW_ACCOUNT_NICKNAME_REGISTERED_BY_ANOTHER_USER	-2
#define Svc_CHECK_NEW_ACCOUNT_EMAIL_NOT_VALID				-3
#define Svc_CHECK_NEW_ACCOUNT_EMAIL_REGISTERED_BY_ANOTHER_USER		-4
#define Svc_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_WITH_ARROBA+1];
   char NewEncryptedPassword[Cry_LENGTH_ENCRYPTED_STR_SHA512_BASE64+1];
   int Result;
   int ReturnCode;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_createAccount;

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

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

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

   /***** Check parameters used to create the new account *****/
   Result = Svc_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 *****/
   strcpy (Gbl.Usrs.Me.UsrDat.Password,NewEncryptedPassword);

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

   /***** Save nickname *****/
   Nck_UpdateMyNick (NewNicknameWithoutArroba);
   strcpy (Gbl.Usrs.Me.UsrDat.Nickname,NewNicknameWithoutArroba);

   /***** Save e-mail *****/
   if (Mai_UpdateEmailInDB (&Gbl.Usrs.Me.UsrDat,userEmail))
     {
      /* E-mail updated sucessfully */
      strcpy (Gbl.Usrs.Me.UsrDat.Email,userEmail);
      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 Svc_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 Svc_CheckParamsNewAccount (char *NewNicknameWithArroba,	// Input
                                      char *NewNicknameWithoutArroba,	// Output
                                      char *NewEmail,			// Input-output
                                      char *NewPlainPassword,		// Input
                                      char *NewEncryptedPassword)	// Output
  {
   char Query[1024];

   /***** Step 1/3: Check new nickname *****/
   /* Make a copy without possible starting arrobas */
   strncpy (NewNicknameWithoutArroba,NewNicknameWithArroba,Nck_MAX_BYTES_NICKNAME_WITH_ARROBA);
   NewNicknameWithoutArroba[Nck_MAX_BYTES_NICKNAME_WITH_ARROBA] = '\0';
   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 *****/
      sprintf (Query,"SELECT COUNT(*) FROM usr_nicknames WHERE Nickname='%s'",
	       NewNicknameWithoutArroba);
      if (DB_QueryCOUNT (Query,"can not check if nickname already existed"))        // A nickname of another user is the same that this nickname
	 return Svc_CHECK_NEW_ACCOUNT_NICKNAME_REGISTERED_BY_ANOTHER_USER;
     }
   else        // New nickname is not valid
      return Svc_CHECK_NEW_ACCOUNT_NICKNAME_NOT_VALID;

   /***** Step 2/3: Check new e-mail *****/
   if (Mai_CheckIfEmailIsValid (NewEmail))	// New e-mail is valid
     {
      /***** Check if the new e-mail matches any of the confirmed e-mails of other users *****/
      sprintf (Query,"SELECT COUNT(*) FROM usr_emails"
		     " WHERE E_mail='%s' AND Confirmed='Y'",
	       NewEmail);
      if (DB_QueryCOUNT (Query,"can not check if e-mail already existed"))	// An e-mail of another user is the same that my e-mail
	 return Svc_CHECK_NEW_ACCOUNT_EMAIL_REGISTERED_BY_ANOTHER_USER;
     }
   else	// New e-mail is not valid
      return Svc_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 Svc_CHECK_NEW_ACCOUNT_PASSWORD_NOT_VALID;

   return Svc_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[512];
   int ReturnCode;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRows;
   char PhotoURL[PATH_MAX+1];

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_loginByUserPasswordKey;

   /***** Allocate space for strings *****/
   loginByUserPasswordKeyOut->wsKey          = (char *) soap_malloc (Gbl.soap,256);
   loginByUserPasswordKeyOut->userNickname   = (char *) soap_malloc (Gbl.soap,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA+1);
   loginByUserPasswordKeyOut->userID         = (char *) soap_malloc (Gbl.soap,256);
   loginByUserPasswordKeyOut->userFirstname  = (char *) soap_malloc (Gbl.soap,256);
   loginByUserPasswordKeyOut->userSurname1   = (char *) soap_malloc (Gbl.soap,256);
   loginByUserPasswordKeyOut->userSurname2   = (char *) soap_malloc (Gbl.soap,256);
   loginByUserPasswordKeyOut->userPhoto      = (char *) soap_malloc (Gbl.soap,PATH_MAX+1);
   loginByUserPasswordKeyOut->userBirthday   = (char *) soap_malloc (Gbl.soap,8+2+2+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 = Svc_GetPlgCodFromAppKey ((const char *) appKey)) != SOAP_OK)
      return ReturnCode;

   /***** Check if user's e-mail, @nickname or ID are valid *****/
   strncpy (UsrIDNickOrEmail,userID,255);
   UsrIDNickOrEmail[255] = '\0';
   if (Nck_CheckIfNickWithArrobaIsValid (UsrIDNickOrEmail))		// 1: It's a nickname
     {
      Str_RemoveLeadingArrobas (UsrIDNickOrEmail);

      /* User has typed a nickname */
      sprintf (Query,"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 e-mail
     {
      /* User has typed an e-mail */
      // TODO: Get only if email confirmed?
      sprintf (Query,"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 e-mail
     {
      // 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?
	 sprintf (Query,"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, e-mail 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 = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get user's data")))	// 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]);

      if (Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,-1L) == SOAP_OK)		// Get some user's data from database
        {
         Gbl.Usrs.Me.Logged = true;
         Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

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

         strncpy (loginByUserPasswordKeyOut->userNickname,Gbl.Usrs.Me.UsrDat.Nickname,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA);
         loginByUserPasswordKeyOut->userNickname[Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA] = '\0';

         strncpy (loginByUserPasswordKeyOut->userID,Gbl.Usrs.Me.UsrDat.IDs.List[0].ID,255);	// TODO: What user's ID?
         loginByUserPasswordKeyOut->userID[255] = '\0';

         strncpy (loginByUserPasswordKeyOut->userSurname1,Gbl.Usrs.Me.UsrDat.Surname1,255);
         loginByUserPasswordKeyOut->userSurname1[255] = '\0';
         strncpy (loginByUserPasswordKeyOut->userSurname2,Gbl.Usrs.Me.UsrDat.Surname2,255);
         loginByUserPasswordKeyOut->userSurname2[255] = '\0';
         strncpy (loginByUserPasswordKeyOut->userFirstname,Gbl.Usrs.Me.UsrDat.FirstName,255);
         loginByUserPasswordKeyOut->userFirstname[255] = '\0';

         Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,PhotoURL,false);
         strncpy (loginByUserPasswordKeyOut->userPhoto,PhotoURL,PATH_MAX);
         loginByUserPasswordKeyOut->userPhoto[PATH_MAX] = '\0';

         strncpy (loginByUserPasswordKeyOut->userBirthday,Gbl.Usrs.Me.UsrDat.Birthday.YYYYMMDD,4+2+2);
         loginByUserPasswordKeyOut->userBirthday[4+2+2] = '\0';

         loginByUserPasswordKeyOut->userRole = Svc_GetRoleFromInternalRole (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB);
        }
      else
	{
	 loginByUserPasswordKeyOut->userCode      = -1;
	 loginByUserPasswordKeyOut->userID        = NULL;
	 loginByUserPasswordKeyOut->userSurname1  = NULL;
	 loginByUserPasswordKeyOut->userSurname2  = NULL;
	 loginByUserPasswordKeyOut->userFirstname = NULL;
	 loginByUserPasswordKeyOut->userPhoto     = NULL;
	 loginByUserPasswordKeyOut->userRole      = 0;
	}
     }

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

   if (NumRows == 1)
      /***** Generate a key used in subsequents calls to other web services *****/
      return Svc_GenerateNewWSKey ((long) loginByUserPasswordKeyOut->userCode,
                                   loginByUserPasswordKeyOut->wsKey);
   else
      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;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRows;
   char PhotoURL[PATH_MAX+1];

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_loginBySessionKey;

   /***** Allocate space for strings *****/
   loginBySessionKeyOut->wsKey          = (char *) soap_malloc (Gbl.soap,256);
   loginBySessionKeyOut->userNickname   = (char *) soap_malloc (Gbl.soap,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA+1);
   loginBySessionKeyOut->userID         = (char *) soap_malloc (Gbl.soap,256);
   loginBySessionKeyOut->userFirstname  = (char *) soap_malloc (Gbl.soap,256);
   loginBySessionKeyOut->userSurname1   = (char *) soap_malloc (Gbl.soap,256);
   loginBySessionKeyOut->userSurname2   = (char *) soap_malloc (Gbl.soap,256);
   loginBySessionKeyOut->userPhoto      = (char *) soap_malloc (Gbl.soap,PATH_MAX+1);
   loginBySessionKeyOut->userBirthday   = (char *) soap_malloc (Gbl.soap,8+2+2+1);
   loginBySessionKeyOut->degreeName     = (char *) soap_malloc (Gbl.soap,256);
   loginBySessionKeyOut->courseName     = (char *) soap_malloc (Gbl.soap,256);

   /***** 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          = 0;	// unknown
   loginBySessionKeyOut->degreeName[0]     = '\0';
   loginBySessionKeyOut->courseName[0]     = '\0';

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

   /***** Check IP of the sender *****/
//   if ((ReturnCode = Svc_CheckIfIPIsAllowed ()) != 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 = Svc_CheckIdSession (sessionID)) != SOAP_OK)
      return ReturnCode;

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

      /***** Get course (row[2]) *****/
      Gbl.CurrentCrs.Crs.CrsCod = Str_ConvertStrCodToLongCod (row[2]);
      Crs_GetDataOfCourseByCod (&Gbl.CurrentCrs.Crs);
      loginBySessionKeyOut->courseCode = (int) Gbl.CurrentCrs.Crs.CrsCod;
      strncpy (loginBySessionKeyOut->courseName,Gbl.CurrentCrs.Crs.FullName,255);
      loginBySessionKeyOut->courseName[255] = '\0';

      /***** Get user code (row[0]) *****/
      Gbl.Usrs.Me.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[0]);
      if (Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod) == SOAP_OK)		// Get some user's data from database
        {
         Gbl.Usrs.Me.Logged = true;
         Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

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

         strncpy (loginBySessionKeyOut->userNickname,Gbl.Usrs.Me.UsrDat.Nickname,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA);
         loginBySessionKeyOut->userNickname[Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA] = '\0';

         strncpy (loginBySessionKeyOut->userID,Gbl.Usrs.Me.UsrDat.IDs.List[0].ID,255);	// TODO: What user's ID?
         loginBySessionKeyOut->userID[255] = '\0';

         strncpy (loginBySessionKeyOut->userSurname1,Gbl.Usrs.Me.UsrDat.Surname1,255);
         loginBySessionKeyOut->userSurname1[255] = '\0';
         strncpy (loginBySessionKeyOut->userSurname2,Gbl.Usrs.Me.UsrDat.Surname2,255);
         loginBySessionKeyOut->userSurname2[255] = '\0';
         strncpy (loginBySessionKeyOut->userFirstname,Gbl.Usrs.Me.UsrDat.FirstName,255);
         loginBySessionKeyOut->userFirstname[255] = '\0';

         Pho_BuildLinkToPhoto (&Gbl.Usrs.Me.UsrDat,PhotoURL,false);
         strncpy (loginBySessionKeyOut->userPhoto,PhotoURL,PATH_MAX);
         loginBySessionKeyOut->userPhoto[PATH_MAX] = '\0';

         strncpy (loginBySessionKeyOut->userBirthday,Gbl.Usrs.Me.UsrDat.Birthday.YYYYMMDD,4+2+2);
         loginBySessionKeyOut->userBirthday[4+2+2] = '\0';

         loginBySessionKeyOut->userRole = Svc_GetRoleFromInternalRole (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB);
        }
      else
	{
	 loginBySessionKeyOut->userCode      = -1;
	 loginBySessionKeyOut->userID        = NULL;
	 loginBySessionKeyOut->userSurname1  = NULL;
	 loginBySessionKeyOut->userSurname2  = NULL;
	 loginBySessionKeyOut->userFirstname = NULL;
	 loginBySessionKeyOut->userPhoto     = NULL;
	 loginBySessionKeyOut->userRole      = 0;
	}

      /***** Get degree (row[1]) *****/
      Gbl.CurrentDeg.Deg.DegCod = Str_ConvertStrCodToLongCod (row[1]);
      Deg_GetDataOfDegreeByCod (&Gbl.CurrentDeg.Deg);
      loginBySessionKeyOut->degreeCode = (int) Gbl.CurrentDeg.Deg.DegCod;
      strncpy (loginBySessionKeyOut->degreeName,Gbl.CurrentDeg.Deg.FullName,255);
      loginBySessionKeyOut->degreeName[255] = '\0';
     }

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

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

   if (NumRows == 1)
      /***** Generate a key used in subsequents calls to other web services *****/
      return Svc_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 e-mail ***********************/
/*****************************************************************************/

int swad__getNewPassword (struct soap *soap,
                          char *userID,char *appKey,				// input
                          struct swad__getNewPasswordOutput *getNewPasswordOut)	// output
  {
   int ReturnCode;
   char UsrIDNickOrEmail[512];
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRows;
   char NewRandomPlainPassword[Pwd_MAX_LENGTH_PLAIN_PASSWORD+1];

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getNewPassword;

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

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

   /***** Check if user's e-mail, @nickname or ID are valid *****/
   strncpy (UsrIDNickOrEmail,userID,255);
   UsrIDNickOrEmail[255] = '\0';
   if (Nck_CheckIfNickWithArrobaIsValid (UsrIDNickOrEmail))		// 1: It's a nickname
     {
      Str_RemoveLeadingArrobas (UsrIDNickOrEmail);

      /* User has typed a nickname */
      sprintf (Query,"SELECT UsrCod"
		     " FROM usr_nicknames"
		     " WHERE Nickname='%s'",
	       UsrIDNickOrEmail);
     }
   else if (Mai_CheckIfEmailIsValid (Gbl.Usrs.Me.UsrIdLogin))		// 2: It's an e-mail
     {
      /* User has typed an e-mail */
      // TODO: Get only if email confirmed?
      sprintf (Query,"SELECT UsrCod"
	             " FROM usr_emails"
		     " WHERE E_mail='%s'",
	       UsrIDNickOrEmail);
     }
   else									// 3: It's not a nickname nor e-mail
     {
      // 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?
	 sprintf (Query,"SELECT UsrCod"
	                " FROM usr_IDs"
			" WHERE UsrID='%s'",
		  UsrIDNickOrEmail);
	}
      else	// String is not a valid user's nickname, e-mail or ID
	 return soap_receiver_fault (Gbl.soap,
				     "Bad log in",
				     "User's e-mail, nickname or ID don't exist");
     }

   /***** Get user's data from database *****/
   if ((NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get user's data")) == 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);	// 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;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   Rol_Role_t Role;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getCourses;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,-1L)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Query my courses from database *****/
   sprintf (Query,"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);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get user's courses");

   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,256);
	 strncpy (getCoursesOut->coursesArray.__ptr[NumRow].courseShortName,row[1],255);
         getCoursesOut->coursesArray.__ptr[NumRow].courseShortName[255] = '\0';

         /* Get course full name (row[2]) */
         getCoursesOut->coursesArray.__ptr[NumRow].courseFullName = (char *) soap_malloc (Gbl.soap,256);
	 strncpy (getCoursesOut->coursesArray.__ptr[NumRow].courseFullName,row[2],255);
         getCoursesOut->coursesArray.__ptr[NumRow].courseFullName[255] = '\0';

         /* Get role (row[3]) */
         if (sscanf (row[3],"%u",&Role) != 1)	// Role in this course
            Role = Rol_UNKNOWN;
         getCoursesOut->coursesArray.__ptr[NumRow].userRole = Svc_GetRoleFromInternalRole (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;
   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
     };

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getCourseInfo;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 = Svc_CheckCourseAndGroupCodes (Gbl.CurrentCrs.Crs.CrsCod,-1L)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      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");
   Inf_GetInfoSrcFromDB (Gbl.CurrentCrs.Crs.CrsCod,InfoType,&InfoSrc,&MustBeRead);
   getCourseInfo->infoSrc = (char *) soap_malloc (Gbl.soap,strlen (NamesInWSForInfoSrc[InfoSrc]) + 1);
   strcpy (getCourseInfo->infoSrc,NamesInWSForInfoSrc[InfoSrc]);

   /***** Set paths *****/
   Deg_InitCurrentCourse ();

   /***** 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 (InfoType)
	   {
	    case Inf_LECTURES:		// Syllabus (lectures)
	    case Inf_PRACTICALS:	// Syllabys (practicals)
	       Result = Syl_WriteSyllabusIntoHTMLBuffer (InfoType,&(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 (InfoType,&(getCourseInfo->infoTxt));
         break;
      case Inf_INFO_SRC_PAGE:		// Web page hosted in SWAD server
	 Result = Inf_WritePageIntoHTMLBuffer (InfoType,&(getCourseInfo->infoTxt));
         break;
      case Inf_INFO_SRC_URL:		// Link to a web page
         getCourseInfo->infoTxt = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_URL+1);
         Inf_WriteURLIntoTxtBuffer (InfoType,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;
  }

/*****************************************************************************/
/********************** Return students in a course **************************/
/*****************************************************************************/
// If groupCode <= 0 ==> get users from the whole course

int swad__getUsers (struct soap *soap,
                    char *wsKey,int courseCode,int groupCode,int userRole,	// input
                    struct swad__getUsersOutput *getUsersOut)			// output
  {
   int ReturnCode;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   bool UsrIDIsVisible;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getUsers;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;
   Gbl.CurrentCrs.Grps.GrpCod = (long) groupCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 = Svc_CheckCourseAndGroupCodes (Gbl.CurrentCrs.Crs.CrsCod,Gbl.CurrentCrs.Grps.GrpCod)) != SOAP_OK)
      return ReturnCode;

   /***** Get some of my data *****/
   if ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

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

   /***** Check requested users' type *****/
   if (userRole != 2 &&	// student
       userRole != 3)	// teacher
     {
      sprintf (Gbl.Message,"Only students (user's type = %u) or teachers (user's type = %u) are allowed",
               (unsigned) 2,	// student
               (unsigned) 3);	// teacher
      return soap_sender_fault (Gbl.soap,
	                        "Bad requested users' type",
	                        Gbl.Message);
     }

   /***** Query users beloging to course or group from database *****/
   if (groupCode <= 0)	// Users belonging to the whole course
      sprintf (Query,"SELECT usr_data.UsrCod"
                     " FROM crs_usr,usr_data"
                     " WHERE crs_usr.CrsCod='%ld' AND crs_usr.UsrCod=usr_data.UsrCod AND crs_usr.Role='%d'"
                     " ORDER BY usr_data.Surname1,usr_data.Surname2,usr_data.FirstName,usr_data.UsrCod",
               (long) courseCode,
               userRole == 2 ? (unsigned) Rol_STUDENT :
                               (unsigned) Rol_TEACHER);
   else			// Users belonging to the group
      sprintf (Query,"SELECT usr_data.UsrCod"
                     " FROM crs_grp_usr,crs_usr,usr_data"
                     " WHERE crs_grp_usr.GrpCod='%ld' AND crs_grp_usr.UsrCod=crs_usr.UsrCod AND crs_grp_usr.UsrCod=usr_data.UsrCod AND crs_usr.Role='%d'"
                     " ORDER BY usr_data.Surname1,usr_data.Surname2,usr_data.FirstName,usr_data.UsrCod",
               (long) groupCode,
               userRole == 2 ? (unsigned) Rol_STUDENT :
                               (unsigned) Rol_TEACHER);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get users");

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

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

      /***** Users' IDs are visible? *****/
      UsrIDIsVisible = (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB == Rol_STUDENT &&
		        userRole == 2);	// get students in the course
      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 user's data */
            if (Usr_ChkUsrCodAndGetAllUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat))
               /* Copy user's data into output structure */
               Svc_CopyUsrData (&(getUsersOut->usersArray.__ptr[NumRow]),&Gbl.Usrs.Other.UsrDat,UsrIDIsVisible);
        }
     }

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

   return SOAP_OK;
  }

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

int swad__getGroupTypes (struct soap *soap,
                         char *wsKey,int courseCode,				// input
                         struct swad__getGroupTypesOutput *getGroupTypesOut)	// output
  {
   int ReturnCode;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   long OpenTime;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getGroupTypes;
   Gbl.CurrentCrs.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 = Svc_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.CurrentCrs.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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

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

   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,256);
	 strncpy (getGroupTypesOut->groupTypesArray.__ptr[NumRow].groupTypeName,row[1],255);
         getGroupTypesOut->groupTypesArray.__ptr[NumRow].groupTypeName[255] = '\0';

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

         /* Get whether user can enroll 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;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   long GrpCod;
   unsigned MaxStudents;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getGroups;
   Gbl.CurrentCrs.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 = Svc_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.CurrentCrs.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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Query groups in a course from database *****/
   sprintf (Query,"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);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get user's groups");

   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,256);
	 strncpy (getGroupsOut->groupsArray.__ptr[NumRow].groupTypeName,row[1],255);
         getGroupsOut->groupsArray.__ptr[NumRow].groupTypeName[255] = '\0';

         /* 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,256);
	 strncpy (getGroupsOut->groupsArray.__ptr[NumRow].groupName,row[3],255);
         getGroupsOut->groupsArray.__ptr[NumRow].groupName[255] = '\0';

         /* 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_CountNumStdsInGrp (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;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   long GrpCod;
   unsigned MaxStudents;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_sendMyGroups;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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.CurrentCrs.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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      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.GrpCod = 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.GrpCod = (long *) calloc (LstGrpsIWant.NumGrps,sizeof (long))) == NULL)
	    Lay_ShowErrorAndExit ("Not enough memory to store the codes of the selected groups.");
         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.GrpCod[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 *****/
   sprintf (Query,"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);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get user's groups");

   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,256);
	 strncpy (SendMyGroupsOut->groupsArray.__ptr[NumRow].groupTypeName,row[1],255);
         SendMyGroupsOut->groupsArray.__ptr[NumRow].groupTypeName[255] = '\0';

         /* 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,256);
	 strncpy (SendMyGroupsOut->groupsArray.__ptr[NumRow].groupName,row[3],255);
         SendMyGroupsOut->groupsArray.__ptr[NumRow].groupName[255] = '\0';

         /* 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_CountNumStdsInGrp (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 Svc_CopyUsrData (struct swad__user *Usr,struct UsrData *UsrDat,bool UsrIDIsVisible)
  {
   char PhotoURL[PATH_MAX+1];

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

   /* Copy user's nickname */
   Usr->userNickname = (char *) soap_malloc (Gbl.soap,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA+1);
   strncpy (Usr->userNickname,UsrDat->Nickname,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA);
   Usr->userNickname[Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA] = '\0';

   /* Copy user's ID */
   Usr->userID = (char *) soap_malloc (Gbl.soap,ID_MAX_LENGTH_USR_ID+1);
   if (UsrIDIsVisible && UsrDat->IDs.List)
     {
      strncpy (Usr->userID,UsrDat->IDs.List[0].ID,ID_MAX_LENGTH_USR_ID);
      Usr->userID[ID_MAX_LENGTH_USR_ID] = '\0';
     }
   else	// Hide user's ID
      strcpy (Usr->userID,"********");

   /* Copy user's surname1 */
   Usr->userSurname1 = (char *) soap_malloc (Gbl.soap,256);
   strncpy (Usr->userSurname1,UsrDat->Surname1,255);
   Usr->userSurname1[255] = '\0';

   /* Copy user's surname2 */
   Usr->userSurname2 = (char *) soap_malloc (Gbl.soap,256);
   strncpy (Usr->userSurname2,UsrDat->Surname2,255);
   Usr->userSurname2[255] = '\0';

   /* Copy user's first name */
   Usr->userFirstname = (char *) soap_malloc (Gbl.soap,256);
   strncpy (Usr->userFirstname,UsrDat->FirstName,255);
   Usr->userFirstname[255] = '\0';

   /* User's photo URL */
   Pho_BuildLinkToPhoto (UsrDat,PhotoURL,false);
   Usr->userPhoto = (char *) soap_malloc (Gbl.soap,PATH_MAX+1);
   strncpy (Usr->userPhoto,PhotoURL,PATH_MAX);
   Usr->userPhoto[PATH_MAX] = '\0';
  }

/*****************************************************************************/
/***************** 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;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   int NumAttEvent;
   long AttCod;
   char PhotoURL[PATH_MAX+1];
   long StartTime;
   long EndTime;
   size_t Length;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getAttendanceEvents;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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.CurrentCrs.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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

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

   /***** Query list of attendance events *****/
   sprintf (Query,"SELECT AttCod,UsrCod,"
                  "UNIX_TIMESTAMP(StartTime) AS ST,UNIX_TIMESTAMP(EndTime) AS ET,"
                  "CommentTchVisible,Title,Txt"
	          " FROM att_events"
                  " WHERE CrsCod='%d' AND Hidden='N'"
                  " ORDER BY ST DESC,ET DESC,Title DESC",
            courseCode);
   getAttendanceEventsOut->eventsArray.__size =
   getAttendanceEventsOut->numEvents          = (int) DB_QuerySELECT (Query,&mysql_res,"can not get attendance events");

   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 user's code of the user who created the event (row[1]) */
         Gbl.Usrs.Other.UsrDat.UsrCod = Str_ConvertStrCodToLongCod (row[1]);
         if (Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,Gbl.CurrentCrs.Crs.CrsCod) == SOAP_OK)	// 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);
            strcpy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userSurname1,
                    Gbl.Usrs.Other.UsrDat.Surname1);

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

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

            Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL,false);
            Length = strlen (PhotoURL);
            getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto = (char *) soap_malloc (Gbl.soap,Length + 1);
            strcpy (getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].userPhoto,
                    PhotoURL);
           }
         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[2]) */
         StartTime = 0L;
         if (row[2])
            sscanf (row[2],"%ld",&StartTime);
         getAttendanceEventsOut->eventsArray.__ptr[NumAttEvent].startTime = StartTime;

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

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

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

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

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

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

      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]);
	 sprintf (GrpCodStr,
		  NumGrp ? ",%ld" :
			   "%ld",
		  GrpCod);
	 strcat (*ListGroups,GrpCodStr);
	}
     }

   /***** 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;
   const char *Ptr;
   char LongStr[1+10+1];
   unsigned NumGrp;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_sendAttendanceEvent;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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.CurrentCrs.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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      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 */
   strncpy (Att.Title,title,Att_MAX_LENGTH_ATTENDANCE_EVENT_TITLE);
   Att.Title[Att_MAX_LENGTH_ATTENDANCE_EVENT_TITLE] = '\0';

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

   /***** Create a list of groups selected *****/
   if (Gbl.CurrentCrs.Grps.LstGrpsSel.NumGrps)
     {
      // Here NestedCalls is always 0
      Gbl.CurrentCrs.Grps.LstGrpsSel.NestedCalls++;
      // Here NestedCalls is always 1
      if ((Gbl.CurrentCrs.Grps.LstGrpsSel.GrpCod = (long *) calloc (Gbl.CurrentCrs.Grps.LstGrpsSel.NumGrps,sizeof (long))) == NULL)
	 Lay_ShowErrorAndExit ("Not enough memory to store the codes of the selected groups.");

      for (Ptr = groups, NumGrp = 0;
	   *Ptr;
	   )
	{
	 Str_GetNextStringUntilComma (&Ptr,LongStr,1+10);
	 Gbl.CurrentCrs.Grps.LstGrpsSel.GrpCod[NumGrp] = Str_ConvertStrCodToLongCod (LongStr);
	 if (Grp_CheckIfGroupBelongsToCourse (Gbl.CurrentCrs.Grps.LstGrpsSel.GrpCod[NumGrp],Gbl.CurrentCrs.Crs.CrsCod))
	    NumGrp++;
	}
      Gbl.CurrentCrs.Grps.LstGrpsSel.NumGrps = NumGrp;	// Update number of 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;
  }

/*****************************************************************************/
/*********** 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];
   char Query[1024];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   char PhotoURL[PATH_MAX+1];
   size_t Length;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getAttendanceUsers;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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.CurrentCrs.Crs.CrsCod = Att.CrsCod;

   /***** Get some of my data *****/
   if ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      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_STUDENT,
	       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_STUDENT,
	       Att.AttCod);
   // Query: list of users in attendance list + rest of users (subquery)
   sprintf (Query,"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);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get users in an attendance event");

   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 (Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,-1L) == SOAP_OK)	// 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);
            strcpy (getAttendanceUsersOut->usersArray.__ptr[NumRow].userNickname,Gbl.Usrs.Other.UsrDat.Nickname);

            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);
	    strcpy (getAttendanceUsersOut->usersArray.__ptr[NumRow].userID,Gbl.Usrs.Other.UsrDat.IDs.List[0].ID);

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

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

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

            Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL,false);
            Length = strlen (PhotoURL);
            getAttendanceUsersOut->usersArray.__ptr[NumRow].userPhoto = (char *) soap_malloc (Gbl.soap,Length + 1);
            strcpy (getAttendanceUsersOut->usersArray.__ptr[NumRow].userPhoto,PhotoURL);
           }
         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];
   long UsrCod;
   unsigned NumCodsInList;
   char SubQuery[256];
   char *Query = NULL;	// Initialized to avoid warning

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_sendAttendanceUsers;

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

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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.CurrentCrs.Crs.CrsCod = Att.CrsCod;

   /***** Get some of my data *****/
   if ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

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

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

      /* Start query used to mark not present users as absent */
      if ((Query = (char *) malloc (256 + NumCodsInList * (1+1+10+1))) == NULL)
	 return soap_receiver_fault (Gbl.soap,
	                             "Not enough memory",
	                             "Not enough memory to store list of users");
      sprintf (Query,"UPDATE att_usr SET Present='N'"
		     " WHERE AttCod='%ld'",
	       Att.AttCod);
     }

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

	       /* Add this user to query used to mark not present users as absent */
	       if (setOthersAsAbsent)
		 {
		  sprintf (SubQuery,sendAttendanceUsersOut->numUsers ? ",'%ld'" :
								       " AND UsrCod NOT IN ('%ld'",
			   UsrCod);
		  strcat (Query,SubQuery);
		 }

	       sendAttendanceUsersOut->numUsers++;
	      }
     }

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

      DB_QueryUPDATE (Query,"can not set other users as absent");
      free ((void *) Query);

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

   sendAttendanceUsersOut->success = 1;

   return SOAP_OK;
  }

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

int swad__getNotifications (struct soap *soap,
                            char *wsKey,long beginTime,					// input
                            struct swad__getNotificationsOutput *getNotificationsOut)	// output
  {
   extern 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;
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRows;
   unsigned NumRow;
   long NtfCod;
   Ntf_NotifyEvent_t NotifyEvent;
   long EventTime;
   char PhotoURL[PATH_MAX+1];
   struct Institution Ins;
   struct Centre Ctr;
   struct Degree Deg;
   struct Course Crs;
   long Cod;
   char ForumName[512];
   char *SummaryStr;
   char *ContentStr;
   Ntf_Status_t Status;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getNotifications;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,-1L)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Get language from database *****/
   if ((ReturnCode = Svc_GetLanguageFromUsrCod (Gbl.Usrs.Me.UsrDat.UsrCod)) != SOAP_OK)
      return ReturnCode;

   if ((SummaryStr = malloc (Cns_MAX_BYTES_TEXT+1)) == NULL)
      return soap_receiver_fault (Gbl.soap,
	                          "Not enough memory",
	                          "Not enough memory to store the summary of the notification");

   /***** Get my notifications from database *****/
   sprintf (Query,"SELECT NtfCod,NotifyEvent,UNIX_TIMESTAMP(TimeNotif),"
	          "FromUsrCod,InsCod,CtrCod,DegCod,CrsCod,Cod,Status"
                  " FROM notif"
                  " WHERE ToUsrCod='%ld' AND TimeNotif>=FROM_UNIXTIME('%ld')"
                  " ORDER BY TimeNotif DESC",
            Gbl.Usrs.Me.UsrDat.UsrCod,beginTime);
   NumRows = DB_QuerySELECT (Query,&mysql_res,"can not get user's notifications");

   getNotificationsOut->numNotifications = (int) NumRows;
   getNotificationsOut->notificationsArray.__size = (int) NumRows;

   if (NumRows == 0)
      getNotificationsOut->notificationsArray.__ptr = NULL;
   else	// Notifications found
     {
      getNotificationsOut->notificationsArray.__ptr = soap_malloc (Gbl.soap,(getNotificationsOut->notificationsArray.__size) * sizeof (*(getNotificationsOut->notificationsArray.__ptr)));

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

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

         /* Get notification event type (row[1]) */
         NotifyEvent = Ntf_GetNotifyEventFromDB ((const char *) row[1]);

         getNotificationsOut->notificationsArray.__ptr[NumRow].eventType = (char *) soap_malloc (Gbl.soap,256);
         strncpy (getNotificationsOut->notificationsArray.__ptr[NumRow].eventType,Ntf_WSNotifyEvents[NotifyEvent],255);
         getNotificationsOut->notificationsArray.__ptr[NumRow].eventType[255] = '\0';

         /* Get time of the event (row[2]) */
         EventTime = 0L;
         if (row[2])
            sscanf (row[2],"%ld",&EventTime);
         getNotificationsOut->notificationsArray.__ptr[NumRow].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 (Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat,Crs.CrsCod) == SOAP_OK)	// Get some user's data from database
           {
            getNotificationsOut->notificationsArray.__ptr[NumRow].userNickname = (char *) soap_malloc (Gbl.soap,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA+1);
            strncpy (getNotificationsOut->notificationsArray.__ptr[NumRow].userNickname,Gbl.Usrs.Other.UsrDat.Nickname,Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA);
            getNotificationsOut->notificationsArray.__ptr[NumRow].userNickname[Nck_MAX_LENGTH_NICKNAME_WITHOUT_ARROBA] = '\0';

            getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname1 = (char *) soap_malloc (Gbl.soap,256);
            strncpy (getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname1,Gbl.Usrs.Other.UsrDat.Surname1,255);
            getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname1[255] = '\0';

            getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname2 = (char *) soap_malloc (Gbl.soap,256);
            strncpy (getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname2,Gbl.Usrs.Other.UsrDat.Surname2,255);
            getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname2[255] = '\0';

            getNotificationsOut->notificationsArray.__ptr[NumRow].userFirstname = (char *) soap_malloc (Gbl.soap,256);
            strncpy (getNotificationsOut->notificationsArray.__ptr[NumRow].userFirstname,Gbl.Usrs.Other.UsrDat.FirstName,255);
            getNotificationsOut->notificationsArray.__ptr[NumRow].userFirstname[255] = '\0';

            Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL,false);
            getNotificationsOut->notificationsArray.__ptr[NumRow].userPhoto = (char *) soap_malloc (Gbl.soap,PATH_MAX+1);
            strncpy (getNotificationsOut->notificationsArray.__ptr[NumRow].userPhoto,PhotoURL,PATH_MAX);
            getNotificationsOut->notificationsArray.__ptr[NumRow].userPhoto[PATH_MAX] = '\0';
           }
         else
           {
            getNotificationsOut->notificationsArray.__ptr[NumRow].userNickname  = NULL;
            getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname1  = NULL;
            getNotificationsOut->notificationsArray.__ptr[NumRow].userSurname2  = NULL;
            getNotificationsOut->notificationsArray.__ptr[NumRow].userFirstname = NULL;
            getNotificationsOut->notificationsArray.__ptr[NumRow].userPhoto     = NULL;
           }

         /* Get institution (row[4]) */
         Ins.InsCod = Str_ConvertStrCodToLongCod (row[4]);
         Ins_GetDataOfInstitutionByCod (&Ins,Ins_GET_MINIMAL_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[NumRow].eventCode = (int) Cod;

         /* Set location */
         getNotificationsOut->notificationsArray.__ptr[NumRow].location = (char *) soap_malloc (Gbl.soap,1024);

	 if (NotifyEvent == Ntf_EVENT_FORUM_POST_COURSE ||
	     NotifyEvent == Ntf_EVENT_FORUM_REPLY)
	   {
            For_SetForumName (For_GetForumTypeOfAPost (Cod),
        	              &Ins,
        	              &Ctr,
        	              &Deg,
        	              &Crs,
        	              ForumName,Gbl.Prefs.Language,false);	// Set forum name in recipient's language
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumRow].location,"%s: %s",
                     Txt_Forum,ForumName);
           }
         else if (Crs.CrsCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumRow].location,"%s: %s",
                     Txt_Course,Crs.ShortName);
         else if (Deg.DegCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumRow].location,"%s: %s",
                     Txt_Degree,Deg.ShortName);
         else if (Ctr.CtrCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumRow].location,"%s: %s",
                     Txt_Centre,Ctr.ShortName);
         else if (Ins.InsCod > 0)
            sprintf (getNotificationsOut->notificationsArray.__ptr[NumRow].location,"%s: %s",
                     Txt_Institution,Ins.ShortName);
         else
            strcpy (getNotificationsOut->notificationsArray.__ptr[NumRow].location,"-");

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

         /* Get summary and content */
         ContentStr = NULL;
         Ntf_GetNotifSummaryAndContent (SummaryStr,&ContentStr,NotifyEvent,
                                        Cod,Crs.CrsCod,Gbl.Usrs.Me.UsrDat.UsrCod,Cfg_MAX_CHARS_NOTIF_SUMMARY_WEB_SERVICE,true);

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

         if (ContentStr != NULL)
           {
            getNotificationsOut->notificationsArray.__ptr[NumRow].content = (char *) soap_malloc (Gbl.soap,strlen (ContentStr)+1);

            strcpy (getNotificationsOut->notificationsArray.__ptr[NumRow].content,ContentStr);

            free ((void *) ContentStr);
            ContentStr = NULL;
           }
         else
           {
            getNotificationsOut->notificationsArray.__ptr[NumRow].content = (char *) soap_malloc (Gbl.soap,1);
            getNotificationsOut->notificationsArray.__ptr[NumRow].content[0] = '\0';
           }
	}
     }

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

   /***** Free summary *****/
   free ((void *) SummaryStr);

   return SOAP_OK;
  }

/*****************************************************************************/
/************ Get language from database giving a user's code ****************/
/*****************************************************************************/
// UsrCod must contain an existing user's code

static int Svc_GetLanguageFromUsrCod (long UsrCod)
  {
   extern const char *Txt_STR_LANG_ID[Txt_NUM_LANGUAGES];
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   Txt_Language_t Lan;

   /***** Get user's language *****/
   sprintf (Query,"SELECT Language FROM usr_data"
	          " WHERE UsrCod='%ld'",
	    UsrCod);
   if (DB_QuerySELECT (Query,&mysql_res,"can not get user's language") != 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 = Cfg_DEFAULT_LANGUAGE_FOR_NEW_USERS;	// Set default language
   for (Lan=(Txt_Language_t) 0;
	Lan < Txt_NUM_LANGUAGES;
	Lan++)
      if (!strcasecmp (row[0],Txt_STR_LANG_ID[Lan]))
        {
	 Gbl.Prefs.Language = Lan;
         break;
        }

   /***** 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;
   char Query[512];

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_markNotificationsAsRead;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,-1L)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   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 *****/
	    sprintf (Query,"UPDATE notif SET Status=(Status | %u)"
			   " WHERE NtfCod='%ld' AND ToUsrCod='%ld'",
		     (unsigned) Ntf_STATUS_BIT_READ,
		     (long) NtfCod,Gbl.Usrs.Me.UsrDat.UsrCod);
	    DB_QueryUPDATE (Query,"can not mark notification as read");

	    NumNtfsMarkedAsRead++;
           }
        }
     }

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

   return SOAP_OK;
  }

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

#define Svc_MAX_LENGHT_QUERY_RECIPIENTS (10*1024)

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

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_sendMessage;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,-1L)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if the message is a reply to a previous message *****/
   if (messageCode)
     {
      /***** Check if the original message was really received by me *****/
      sprintf (Query,"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);
      if (!DB_QuerySELECT (Query,&mysql_res,"can not check original message"))
         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 *****/
      sprintf (Query,"SELECT UsrCod FROM msg_snt"
                     " WHERE MsgCod='%ld'"
                     " UNION "
                     "SELECT UsrCod FROM msg_snt_deleted"
                     " WHERE MsgCod='%ld'",
               (long) messageCode,(long) messageCode);
      if ((NumRows = DB_QuerySELECT (Query,&mysql_res,"can not check original message")))	// 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");
     }


   /***** Build query for recipients from database *****/
   if (ReplyUsrCod > 0)
      sprintf (Query,"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_WITH_ARROBA);

      /* 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_LENGTH_NICKNAME_WITHOUT_ARROBA+32 > Svc_MAX_LENGHT_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)
	       strcat (Query," UNION ");
	    strcat (Query,"SELECT UsrCod FROM usr_nicknames"
			  " WHERE Nickname IN ('");
	    FirstNickname = false;
	    ThereAreNicknames = true;
	   }
	 else
	    strcat (Query,",'");
	 strcat (Query,Nickname);
	 strcat (Query,"'");
	}
     }
   if (ThereAreNicknames)
      strcat (Query,")");

   /***** 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 (Query,&mysql_res,"can not get users");

      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))
        	 {
                  /* This received message must be notified by e-mail? */
                  NotifyByEmail = ((Gbl.Usrs.Other.UsrDat.UsrCod != Gbl.Usrs.Me.UsrDat.UsrCod) &&
                                   (Gbl.Usrs.Other.UsrDat.Prefs.EmailNtfEvents & (1 << Ntf_EVENT_MESSAGE)));

                  /* Send message to this user */
                  if ((ReturnCode = Svc_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 */
                  Svc_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 ************************/
/*****************************************************************************/

static int Svc_sendMessageToUsr (long RepliedMsgCod,long SenderUsrCod,long ReplyUsrCod,long RecipientUsrCod,bool NotifyByEmail,const char *Subject,const char *Content)
  {
   static bool MsgAlreadyInserted = false;
   static long NewMsgCod;
   char Query[512+Cns_MAX_BYTES_SUBJECT+Cns_MAX_BYTES_LONG_TEXT];

   /***** Create message *****/
   if (!MsgAlreadyInserted)      // The message is inserted only once in the table of messages sent
     {
      /***** Insert message subject and body in the database *****/
      /* Build query */
      sprintf (Query,"INSERT INTO msg_content (Subject,Content)"
                     " VALUES ('%s','%s')",Subject,Content);

      /* Get the code of the inserted item */
      NewMsgCod = DB_QueryINSERTandReturnCode (Query,"can not create message");

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

      MsgAlreadyInserted = true;
     }

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

   /***** Create notification for this recipient.
          If this recipient wants to receive notifications by e-mail, activate the sending of a notification *****/
   sprintf (Query,"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));
   DB_QueryINSERT (Query,"can not create new notification event");

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

   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;
   char Query[512+Cns_MAX_BYTES_TEXT];
   long NotCod;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_sendNotice;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

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

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

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

   /***** Insert notice in the database *****/
   /* Build query */
   sprintf (Query,"INSERT INTO notices (CrsCod,UsrCod,CreatTime,Content,Status)"
                  " VALUES ('%ld','%ld',NOW(),'%s','%u')",
            Gbl.CurrentCrs.Crs.CrsCod,Gbl.Usrs.Me.UsrDat.UsrCod,
            body,(unsigned) Not_ACTIVE_NOTICE);

   /* Get the code of the inserted item */
   NotCod = DB_QueryINSERTandReturnCode (Query,"can not create message");

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

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getTestConfig;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if course code is correct *****/
   if (Gbl.CurrentCrs.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 or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get degree of current course *****/
   if ((ReturnCode = Svc_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,256);
   getTestConfigOut->feedback[0] = '\0';

   /***** Get test configuration *****/
   if ((ReturnCode = Svc_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;
   // if (Gbl.Test.Config.FeedbackType == Tst_FEEDBACK_FULL_FEEDBACK)
   //   Gbl.Test.Config.FeedbackType = Tst_FEEDBACK_EACH_GOOD_BAD;	// TODO: remove this when SWADroid uses it
   strncpy (getTestConfigOut->feedback,Tst_FeedbackXML[Gbl.Test.Config.FeedbackType],255);
   getTestConfigOut->feedback[255] = '\0';

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

   return SOAP_OK;
  }

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

static int Svc_GetTstConfig (long CrsCod)
  {
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;

   /***** Query database *****/
   sprintf (Query,"SELECT Pluggable,Min,Def,Max,MinTimeNxtTstPerQst,Feedback"
                  " FROM tst_config WHERE CrsCod='%ld'",
            CrsCod);
   if (DB_QuerySELECT (Query,&mysql_res,"can not get test configuration"))
     {
      /***** 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.FeedbackType = 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 Svc_GetNumTestQuestionsInCrs (long CrsCod)
  {
   char Query[512];

   /***** Get number of questions *****/
   // Reject questions with any tag hidden
   // Select only questions with tags
   sprintf (Query,"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 (int) DB_QueryCOUNT (Query,"can not get number of test questions");
  }

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

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getTests;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if course code is correct *****/
   if (Gbl.CurrentCrs.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 or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Get degree of current course *****/
   if ((ReturnCode = Svc_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 = Svc_GetTstConfig ((long) courseCode)) != SOAP_OK)
      return ReturnCode;

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

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

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

      /***** Get tags for each question *****/
      if ((ReturnCode = Svc_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 Svc_GetTstTags (long CrsCod,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Txt_STR_LANG_ID[Txt_NUM_LANGUAGES];
   char Query[512];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;

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

   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,256);
	 strncpy (getTestsOut->tagsArray.__ptr[NumRow].tagText,row[1],255);
         getTestsOut->tagsArray.__ptr[NumRow].tagText[255] = '\0';
	}
     }

   /***** 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 Svc_GetTstQuestions (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
   char Query[1024];
   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
   sprintf (Query,"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);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get test questions");

   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,256);
	 strncpy (getTestsOut->questionsArray.__ptr[NumRow].answerType,Tst_StrAnswerTypesXML[AnswerType],255);
         getTestsOut->questionsArray.__ptr[NumRow].answerType[255] = '\0';

         /* 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);
         strncpy (getTestsOut->questionsArray.__ptr[NumRow].stem,row[3],Cns_MAX_BYTES_TEXT);
         getTestsOut->questionsArray.__ptr[NumRow].stem[Cns_MAX_BYTES_TEXT] = '\0';

         /* Get question feedback (row[4]) */
         getTestsOut->questionsArray.__ptr[NumRow].feedback = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_TEXT+1);
         strncpy (getTestsOut->questionsArray.__ptr[NumRow].feedback,row[4],Cns_MAX_BYTES_TEXT);
         getTestsOut->questionsArray.__ptr[NumRow].feedback[Cns_MAX_BYTES_TEXT] = '\0';
	}
     }

   /***** 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 Svc_GetTstAnswers (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
   char Query[1024];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   unsigned Index;

   /***** Get recent test questions from database *****/
   sprintf (Query,"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);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get test answers");

   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);
         strncpy (getTestsOut->answersArray.__ptr[NumRow].answerText,row[3],Cns_MAX_BYTES_TEXT);
         getTestsOut->answersArray.__ptr[NumRow].answerText[Cns_MAX_BYTES_TEXT] = '\0';

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

   /***** 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 Svc_GetTstQuestionTags (long CrsCod,long BeginTime,struct swad__getTestsOutput *getTestsOut)
  {
   extern const char *Tst_StrAnswerTypesXML[Tst_NUM_ANS_TYPES];
   char Query[1024];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow,NumRows;
   unsigned Index;

   /***** Get recent test questions from database *****/
   sprintf (Query,"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);
   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get test question tags");

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

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[1024];
   char DegStr[ 1+1+1+  10  +1+ 1];
   //   DegStr=", ' - number ' \0"
   long DegCod;
   bool FirstDegree = true;
   char Query[4096];
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumRow;
   unsigned NumRows;
   long QstCod = -1L;
   Tst_AnswerType_t AnswerType;
   unsigned Index;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getTrivialQuestion;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Loop over recipients' nicknames building query *****/
   DegreesStr[0] = '\0';
   Ptr = degrees;

      if (Gbl.Usrs.Me.UsrDat.UsrCod == 19543)
	{
	 char QueryDebug[512*1024];

	 sprintf (QueryDebug,"INSERT INTO debug (DebugTime,Txt) VALUES (NOW(),'degrees = %s')",degrees);
	 DB_QueryINSERT (QueryDebug,"Error inserting in debug table");
	}

   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)
	      {
	       sprintf (DegreesStr,"'%ld'",DegCod);
	       FirstDegree = false;
	      }
	    else
	      {
	       sprintf (DegStr,",'%ld'",DegCod);
	       strcat (DegreesStr,DegStr);
	      }
	   }
     }

   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 *****/
   setlocale (LC_NUMERIC,"en_US.utf8");	// To print the floating point as a dot
   sprintf (Query,"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);
   setlocale (LC_NUMERIC,"es_ES.utf8");	// Return to spanish system (TODO: this should be internationalized!!!!!!!)

   NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get test questions");

   if (NumRows == 1)	// Question found
     {
      if (Gbl.Usrs.Me.UsrDat.UsrCod == 19543)
	{
	 char QueryDebug[512*1024];

	 sprintf (QueryDebug,"INSERT INTO debug (DebugTime,Txt) VALUES (NOW(),'Una pregunta devuelta')");
	 DB_QueryINSERT (QueryDebug,"Error inserting in debug table");
	}

      /* 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,256);
      strncpy (getTrivialQuestionOut->question.answerType,Tst_StrAnswerTypesXML[AnswerType],255);
      getTrivialQuestionOut->question.answerType[255] = '\0';

      /* 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);
      strncpy (getTrivialQuestionOut->question.stem,row[3],Cns_MAX_BYTES_TEXT);
      getTrivialQuestionOut->question.stem[Cns_MAX_BYTES_TEXT] = '\0';

      /* Get question feedback (row[4]) */
      getTrivialQuestionOut->question.feedback = (char *) soap_malloc (Gbl.soap,Cns_MAX_BYTES_TEXT+1);
      strncpy (getTrivialQuestionOut->question.feedback,row[4],Cns_MAX_BYTES_TEXT);
      getTrivialQuestionOut->question.feedback[Cns_MAX_BYTES_TEXT] = '\0';
     }
   else		// Empty question
     {
      if (Gbl.Usrs.Me.UsrDat.UsrCod == 19543)
	{
	 char QueryDebug[512*1024];

	 sprintf (QueryDebug,"INSERT INTO debug (DebugTime,Txt) VALUES (NOW(),'Ninguna pregunta devuelta')");
	 DB_QueryINSERT (QueryDebug,"Error inserting in debug table");
	}

      /* 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 *****/
      sprintf (Query,"SELECT QstCod,AnsInd,Correct,Answer,Feedback"
		     " FROM tst_answers WHERE QstCod='%ld'"
		     " ORDER BY AnsInd",
	       QstCod);
      NumRows = (unsigned) DB_QuerySELECT (Query,&mysql_res,"can not get test answers");

      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);
	    strncpy (getTrivialQuestionOut->answersArray.__ptr[NumRow].answerText,row[3],Cns_MAX_BYTES_TEXT);
	    getTrivialQuestionOut->answersArray.__ptr[NumRow].answerText[Cns_MAX_BYTES_TEXT] = '\0';

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

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

   return SOAP_OK;
  }

/*****************************************************************************/
/*************** 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 PathXMLPriv[PATH_MAX+1];
   char XMLFileName[PATH_MAX+1];
   unsigned long FileSize;
   unsigned long NumBytesRead;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getDirectoryTree;
   Gbl.CurrentCrs.Crs.CrsCod = (long) courseCode;
   Gbl.CurrentCrs.Grps.GrpCod = (long) groupCode;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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 ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

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

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

   /***** Check if I belong to course/group *****/
   if (Gbl.CurrentCrs.Grps.GrpCod > 0)
      if (!Grp_GetIfIBelongToGrp (Gbl.CurrentCrs.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_DOCUM_GRP;
	       break;
	    case 2:	// Shared files
	       Gbl.FileBrowser.Type = Brw_ADMI_SHARE_GRP;
	       break;
	    case 3:	// Marks
	       Gbl.FileBrowser.Type = Brw_SHOW_MARKS_GRP;
	       break;
	   }
      else	// groupCode <= 0
	 switch (treeCode)
	   {
	    case 1:	// Documents
	       Gbl.FileBrowser.Type = Brw_SHOW_DOCUM_CRS;
	       break;
	    case 2:	// Shared files
	       Gbl.FileBrowser.Type = Brw_ADMI_SHARE_CRS;
	       break;
	    case 3:	// Marks
	       Gbl.FileBrowser.Type = Brw_SHOW_MARKS_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.CurrentCrs.Crs.CrsCod = (courseCode > 0) ? (long) courseCode :
	                                          -1L;
   Gbl.CurrentCrs.Grps.GrpCod = (groupCode > 0) ? (long) groupCode :
	                                          -1L;

   sprintf (Gbl.CurrentCrs.PathPriv,"%s/%s/%ld",
            Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_CRS,Gbl.CurrentCrs.Crs.CrsCod);
   Brw_InitializeFileBrowser ();
   Brw_SetFullPathInTree (Brw_RootFolderInternalNames[Gbl.FileBrowser.Type],".");

   /* Check if exists the directory for HTML output. If not exists, create it */
   sprintf (PathXMLPriv,"%s/%s",Cfg_PATH_SWAD_PRIVATE,Cfg_FOLDER_OUT);
   Fil_CreateDirIfNotExists (PathXMLPriv);

   /* Create a unique name for the file */
   sprintf (XMLFileName,"%s/%s.xml",
            PathXMLPriv,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.Priv.FullPathInTree)) // If root folder is visible
      Svc_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 Svc_ListDir (unsigned Level,const char *Path,const char *PathInTree)
  {
   extern const char *Txt_NEW_LINE;
   struct dirent **DirFileList;
   int NumFileInThisDir;
   int NumFilesInThisDir;
   char PathFileRel[PATH_MAX+1];
   char PathFileInExplTree[PATH_MAX+1];
   struct stat FileStatus;

   /***** Scan directory *****/
   NumFilesInThisDir = scandir (Path,&DirFileList,NULL,alphasort);

   /***** List files *****/
   for (NumFileInThisDir = 0;
	NumFileInThisDir < NumFilesInThisDir;
	NumFileInThisDir++)
      if (strcmp (DirFileList[NumFileInThisDir]->d_name,".") &&
          strcmp (DirFileList[NumFileInThisDir]->d_name,".."))	// Skip directories "." and ".."
        {
	 sprintf (PathFileRel       ,"%s/%s",Path      ,DirFileList[NumFileInThisDir]->d_name);
	 sprintf (PathFileInExplTree,"%s/%s",PathInTree,DirFileList[NumFileInThisDir]->d_name);

	 lstat (PathFileRel,&FileStatus);

         /***** Construct the full path of the file or folder *****/
         Brw_SetFullPathInTree (PathInTree,DirFileList[NumFileInThisDir]->d_name);

	 if (S_ISDIR (FileStatus.st_mode))	// It's a directory
	   {
            /***** Write a row for the subdirectory *****/
	    if (Svc_WriteRowFileBrowser (Level,Brw_IS_FOLDER,DirFileList[NumFileInThisDir]->d_name))
	      {
               /* List subtree starting at this this directory */
               Svc_ListDir (Level+1,PathFileRel,PathFileInExplTree);

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

/*****************************************************************************/
/*********************** 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 Svc_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[PATH_MAX+1];

   /***** Is this row hidden or visible? *****/
   if (Gbl.FileBrowser.Type == Brw_SHOW_DOCUM_CRS ||
       Gbl.FileBrowser.Type == Brw_SHOW_DOCUM_GRP)
      if (Brw_CheckIfFileOrFolderIsSetAsHiddenInDB (FileType,
                                                    Gbl.FileBrowser.Priv.FullPathInTree))
	 return false;

   /***** XML row *****/
   /* Indent */
   Svc_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.FileType,
	                                        Gbl.FileBrowser.Priv.FullPathInTree,false,Brw_LICENSE_DEFAULT);

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

      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 Svc_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 PathUntilFileName[PATH_MAX+1];
   char FileName[NAME_MAX+1];
   char URL[PATH_MAX+1];
   char PhotoURL[PATH_MAX+1];

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getFile;

   /***** Allocate space for strings *****/
   getFileOut->fileName        = (char *) soap_malloc (Gbl.soap,NAME_MAX+1);
   getFileOut->URL             = (char *) soap_malloc (Gbl.soap,PATH_MAX+1);
   getFileOut->license         = (char *) soap_malloc (Gbl.soap,256);
   getFileOut->publisherName   = (char *) soap_malloc (Gbl.soap,255+1+255+1+255+1);
   getFileOut->publisherPhoto  = (char *) soap_malloc (Gbl.soap,PATH_MAX+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 = Svc_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.CurrentCrs.Crs.CrsCod,
                                  &Gbl.CurrentCrs.Grps.GrpCod);

   /***** Get some of my data *****/
   if ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

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

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

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

   /***** Check if file is in a valid zone *****/
   switch ((Gbl.FileBrowser.Type = FileMetadata.FileBrowser))
     {
      // case Brw_SHOW_DOCUM_CRS:	// This type of file browser is not in database
      // case Brw_SHOW_DOCUM_GRP:	// This type of file browser is not in database
      case Brw_ADMI_DOCUM_CRS:
      case Brw_ADMI_DOCUM_GRP:
      case Brw_ADMI_SHARE_CRS:
      case Brw_ADMI_SHARE_GRP:
         break;
      case Brw_ADMI_MARKS_CRS:
      case Brw_ADMI_MARKS_GRP:
	 // Downloading a file of marks is only allowed for teachers
	 if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
	    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 *****/
   Deg_InitCurrentCourse ();
   Str_SplitFullPathIntoPathAndFileName (FileMetadata.Path,PathUntilFileName,FileName);
   Brw_SetFullPathInTree (PathUntilFileName,FileName);
   Brw_InitializeFileBrowser ();

   /***** 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 (PathUntilFileName,
	                      FileName,
	                      URL);

   /***** Copy data into output structure *****/
   strncpy (getFileOut->fileName,FileName,NAME_MAX);
   getFileOut->fileName[NAME_MAX] = '\0';

   strncpy (getFileOut->URL,URL,PATH_MAX);
   getFileOut->URL[PATH_MAX] = '\0';

   getFileOut->size = (int) FileMetadata.Size;

   getFileOut->time = (int) FileMetadata.Time;

   strncpy (getFileOut->license,Txt_LICENSES[FileMetadata.License],255);
   getFileOut->license[255] = '\0';

   if ((Gbl.Usrs.Other.UsrDat.UsrCod = FileMetadata.PublisherUsrCod) > 0)
      /* Get publisher's data */
      if (Usr_ChkUsrCodAndGetAllUsrDataFromUsrCod (&Gbl.Usrs.Other.UsrDat))
	 /* Copy publisher's data into output structure */
	{
	 strncpy (getFileOut->publisherName,Gbl.Usrs.Other.UsrDat.FullName,255+1+255+1+255);
	 getFileOut->publisherName[255+1+255+1+255] = '\0';

	 Pho_BuildLinkToPhoto (&Gbl.Usrs.Other.UsrDat,PhotoURL,false);
	 strncpy (getFileOut->publisherPhoto,PhotoURL,PATH_MAX);
	 getFileOut->publisherPhoto[PATH_MAX] = '\0';
	}

   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[NAME_MAX+1];	// Really not used
   char *ContentStr;

   Gbl.soap = soap;
   Gbl.WebService.Function = Svc_getMarks;
   getMarksOut->content = NULL;

   /***** Check web service key *****/
   if ((ReturnCode = Svc_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.FileType != Brw_IS_FILE ||
       FileMetadata.IsHidden ||
       (FileMetadata.FileBrowser != Brw_ADMI_MARKS_CRS &&
	FileMetadata.FileBrowser != Brw_ADMI_MARKS_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.CurrentCrs.Crs.CrsCod,
                                  &Gbl.CurrentCrs.Grps.GrpCod);

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

   /***** Get some of my data *****/
   if ((ReturnCode = Svc_GetSomeUsrDataFromUsrCod (&Gbl.Usrs.Me.UsrDat,Gbl.CurrentCrs.Crs.CrsCod)) != SOAP_OK)
      return ReturnCode;
   Gbl.Usrs.Me.Logged = true;
   Gbl.Usrs.Me.LoggedRole = Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB;

   /***** Check if I am a student or teacher in the course *****/
   if (Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_STUDENT &&
       Gbl.Usrs.Me.UsrDat.RoleInCurrentCrsDB != Rol_TEACHER)
      return soap_receiver_fault (Gbl.soap,
	                          "Request forbidden",
	                          "Requester must belong to course");

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

   /***** Get content *****/
   ContentStr = NULL;
   Mrk_GetNotifMyMarks (SummaryStr,&ContentStr,
                        FileMetadata.FilCod,Gbl.Usrs.Me.UsrDat.UsrCod,
                        0,true);
   if (ContentStr != NULL)
     {
      getMarksOut->content = (char *) soap_malloc (Gbl.soap,strlen (ContentStr)+1);

      strcpy (getMarksOut->content,ContentStr);

      free ((void *) ContentStr);
      ContentStr = NULL;
     }
   else
     {
      getMarksOut->content = (char *) soap_malloc (Gbl.soap,1);
      getMarksOut->content[0] = '\0';
     }

   return SOAP_OK;
  }
