Beispiel #1
0
        public async Task <Dictionary <string, object> > GetSyncResponse(int eventId, int teamId, int puzzleId, List <int> query_puzzle_ids,
                                                                         int?min_solve_count, string annotations, string last_sync_time)
        {
            SyncResponse response = new SyncResponse();

            // Get information for the puzzle that's being sync'ed.

            Puzzle thisPuzzle = await context.Puzzles.Where(puz => puz.ID == puzzleId).FirstOrDefaultAsync();

            if (thisPuzzle == null)
            {
                response.AddError("Could not find the puzzle you tried to sync.");
                return(response.GetResult());
            }

            // Check to make sure that they're not trying to sync a puzzle and event that don't go together.
            // That could be a security issue, allowing them to unlock pieces for a puzzle using the progress
            // they made in a whole different event!

            if (thisPuzzle.Event.ID != eventId)
            {
                response.AddError("That puzzle doesn't belong to that event.");
                return(response.GetResult());
            }

            // Get the site version for this puzzle, so that if it's changed since the last time the
            // requester sync'ed, the requester will know to reload itself.

            response.SetSiteVersion(GetSiteVersion(thisPuzzle));

            // Decode the request.  If there are any errors, return an error response.

            DecodedSyncRequest request = new DecodedSyncRequest(query_puzzle_ids, min_solve_count, annotations, last_sync_time,
                                                                thisPuzzle.MaxAnnotationKey, ref response);

            // Do any processing that requires fetching the list of all puzzles this team has
            // solved.  Pass thisPuzzle.Group as the groupExcludedFromSolveCount parameter so that,
            // when counting solves, we don't count solves in the same group as this puzzle as part
            // of the solve count.

            await HandleSyncAspectsRequiringListOfSolvedPuzzles(request, response, thisPuzzle.Group, teamId, eventId);

            // Store any annotations the requester provided

            await StoreAnnotations(request, response, thisPuzzle.ID, teamId);

            // Fetch and return any annotations that the requester may not have yet

            await GetAnnotationsRequesterLacks(request, response, thisPuzzle.ID, teamId);

            return(response.GetResult());
        }
        /// <summary>
        ///   This routine updates a single existing annotation.  As we do so, we increment
        ///   its version number and update its timestamp.
        ///
        ///   This routine is only meant to be called when we know an annotation exists
        ///   with the given puzzle ID, team ID, and key.  In other words, this routine
        ///   is called when we need to update that annotation.
        /// </summary>
        private async Task UpdateOneAnnotation(SyncResponse response, int puzzleId, int teamId, int key, string contents)
        {
            // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal"
            // Entity Framework database functions.  The answer is that we need to atomically
            // update the Version field of the record, and Entity Framework has no way of
            // expressing that directly.
            //
            // The reason we want to update the version number atomically is that we rely
            // on the version number being a unique identifier of an annotation.  We don't
            // want the following scenario:
            //
            // Alice tries to set the annotation for key 17 to A, and simultaneously Bob
            // tries to set it to B.  Each reads the current version number, finds it to be
            // 3, and updates the annotation to have version 4.  Both of these updates may
            // succeed, but one will overwrite the other; let's say Bob's write happens last
            // and "wins".  So Alice may believe that version 4 is A when actually version 4
            // is B.  When Alice asks for the current version, she'll be told it's version 4,
            // and Alice will believe this means it's A.  So Alice will believe that A is
            // what's stored in the database even though it's not.  Alice and Bob's computers
            // will display different annotations for the same key, indefinitely.
            //
            // Note that we need a version number because the timestamp isn't guaranteed to
            // be unique.  So in the example above Alice and Bob might wind up updating with
            // the same timestamp.
            //
            // You may also wonder why we use DateTime.Now instead of letting the database
            // assign the timestamp itself.  The reason is that the database might be running
            // on a different machine than the puzzle server, and it might be using a different
            // time zone.

            try {
                var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key";
                int result     = await context.Database.ExecuteSqlRawAsync(sqlCommand,
                                                                           new SqlParameter("@Contents", contents),
                                                                           new SqlParameter("@Timestamp", DateTime.Now),
                                                                           new SqlParameter("@PuzzleID", puzzleId),
                                                                           new SqlParameter("@TeamID", teamId),
                                                                           new SqlParameter("@Key", key));

                if (result != 1)
                {
                    response.AddError("Annotation update failed.");
                }
            }
            catch (DbUpdateException) {
                response.AddError("Encountered error while trying to update annotation.");
            }
            catch (Exception) {
                response.AddError("Miscellaneous error while trying to update annotation.");
            }
        }
            public DecodedSyncRequest(List <int> query_puzzle_ids, int?min_solve_count, string annotations, string last_sync_time,
                                      int maxAnnotationKey, bool query_all_in_group, ref SyncResponse response)
            {
                // The query_puzzle_ids, min_solve_count, and query_all_in_group fields are already perfectly fine, so just copy them.

                QueryPuzzleIds  = query_puzzle_ids;
                MinSolveCount   = min_solve_count;
                QueryAllInGroup = query_all_in_group;

                // The last_sync_time field is a JSON-converted DateTime.  Or at least, it's supposed to be;
                // we need to check.  Note that this string is one that we (the server) encoded.  We do DateTime
                // encoding and decoding on the server so as not to worry about different browsers encoding
                // DateTimes differently.

                if (last_sync_time == null)
                {
                    LastSyncTime = null;
                }
                else
                {
                    try {
                        LastSyncTime = JsonConvert.DeserializeObject <DateTime>(last_sync_time);
                    }
                    catch (JsonException) {
                        response.AddError("Could not deserialize last sync time.");
                        LastSyncTime = null;
                    }
                }

                // The annotations field is a JSON-converted list of objects, each with an integer key and
                // string contents.  Or at least, it's supposed to be; we have to check it carefully.

                AnnotationRequests = new List <AnnotationRequest>();
                if (annotations != null)
                {
                    List <AnnotationRequest> uncheckedAnnotationRequests;

                    try {
                        // First, decode it into a list using the JSON deserializer.
                        uncheckedAnnotationRequests = JsonConvert.DeserializeObject <List <AnnotationRequest> >(annotations);
                    }
                    catch (JsonException) {
                        response.AddError("Could not deserialize annotations list.");
                        uncheckedAnnotationRequests = new List <AnnotationRequest>();
                    }

                    // Check each of the elements for validity.

                    foreach (var annotation in uncheckedAnnotationRequests)
                    {
                        if (annotation.key < 1)
                        {
                            response.AddError("Found non-positive key in annotation.");
                            continue;
                        }
                        if (annotation.key > maxAnnotationKey)
                        {
                            response.AddError("Found too-high key in annotation.");
                            continue;
                        }
                        if (annotation.contents.Length > 255)
                        {
                            response.AddError("Found contents in annotation longer than 255 bytes.");
                            continue;
                        }
                        AnnotationRequests.Add(annotation);
                    }
                }
            }
Beispiel #4
0
        /// <summary>
        ///   This routine stores in the database any annotations the requester has uploaded.
        /// </summary>
        private async Task StoreAnnotations(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId)
        {
            if (request.AnnotationRequests == null)
            {
                return;
            }

            foreach (var annotationRequest in request.AnnotationRequests)
            {
                // Try to generate this as a new annotation, with version 1.

                Annotation annotation = new Annotation();
                annotation.PuzzleID  = puzzleId;
                annotation.TeamID    = teamId;
                annotation.Key       = annotationRequest.key;
                annotation.Version   = 1;
                annotation.Contents  = annotationRequest.contents;
                annotation.Timestamp = DateTime.Now;

                try {
                    context.Annotations.Add(annotation);
                    await context.SaveChangesAsync();
                }
                catch (DbUpdateException) {
                    // If the insert fails, there must already be an annotation there with the
                    // same puzzle ID, team ID, and key.  So we need to update the existing one.
                    // As we do so, we increment its version number and update its timestamp.
                    //
                    // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal"
                    // Entity Framework database functions.  The answer is that we need to atomically
                    // update the Version field of the record, and Entity Framework has no way of
                    // expressing that directly.
                    //
                    // The reason we want to update the version number atomically is that we rely
                    // on the version number being a unique identifier of an annotation.  We don't
                    // want the following scenario:
                    //
                    // Alice tries to set the annotation for key 17 to A, and simultaneously Bob
                    // tries to set it to B.  Each reads the current version number, finds it to be
                    // 3, and updates the annotation to have version 4.  Both of these updates may
                    // succeed, but one will overwrite the other; let's say Bob's write happens last
                    // and "wins".  So Alice may believe that version 4 is A when actually version 4
                    // is B.  When Alice asks for the current version, she'll be told it's version 4,
                    // and Alice will believe this means it's A.  So Alice will believe that A is
                    // what's stored in the database even though it's not.  Alice and Bob's computers
                    // will display different annotations for the same key, indefinitely.
                    //
                    // Note that we need a version number because the timestamp isn't guaranteed to
                    // be unique.  So in the example above Alice and Bob might wind up updating with
                    // the same timestamp.
                    //
                    // You may also wonder why we use DateTime.Now instead of letting the database
                    // assign the timestamp itself.  The reason is that the database might be running
                    // on a different machine than the puzzle server, and it might be using a different
                    // time zone.

                    // First, detach the annotation from the context so the context doesn't think the annotation is in the database.
                    context.Entry(annotation).State = EntityState.Detached;

                    try {
                        var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key";
                        int result     = await context.Database.ExecuteSqlCommandAsync(sqlCommand,
                                                                                       new SqlParameter("@Contents", annotationRequest.contents),
                                                                                       new SqlParameter("@Timestamp", DateTime.Now),
                                                                                       new SqlParameter("@PuzzleID", puzzleId),
                                                                                       new SqlParameter("@TeamID", teamId),
                                                                                       new SqlParameter("@Key", annotationRequest.key));

                        if (result != 1)
                        {
                            response.AddError("Annotation update failed.");
                        }
                    }
                    catch (DbUpdateException) {
                        response.AddError("Encountered error while trying to update annotation.");
                    }
                    catch (Exception) {
                        response.AddError("Miscellaneous error while trying to update annotation.");
                    }
                }
            }
        }