Compare commits

...
This repository has been archived on 2025-06-10. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

95 commits

Author SHA1 Message Date
5652da5ade [WIP] admin: new file regrouping all teams stats 2017-04-05 02:08:03 +02:00
6765e6af32 frontend: improve home page 2017-04-05 02:06:44 +02:00
6fab208a23 backend: simplify condition 2017-04-05 02:06:44 +02:00
7c07cbb010 admin: improve design of settings page 2017-04-05 02:06:43 +02:00
77c6996ecc admin: manage team certificate from interface 2017-04-05 02:06:43 +02:00
cd0d6d7cfb admin: unify API to revoke certificates 2017-04-05 02:06:42 +02:00
c0433ce4ab frontend: new page that list videos 2017-04-05 02:06:42 +02:00
ce901fbfed admin: Add a page to list teams and members 2017-04-05 02:06:41 +02:00
f1fb33aa4d settings: add title and authors 2017-04-05 02:06:40 +02:00
f4696d0e75 admin: fix and generalize team stats 2017-04-05 02:06:40 +02:00
70093b98e7 admin: add danger alert in select 2017-04-05 02:06:39 +02:00
16d6813e9c Move PKI scripts at root 2017-04-05 02:06:39 +02:00
a2a99c6873 frontend: use ng-cloak and ng-if 2017-04-05 02:06:38 +02:00
103476eabe Add password paper generator 2017-04-05 02:06:37 +02:00
704cfeb22e Compute hint mime type in a variable and display it instead of the hint content 2017-04-05 02:06:37 +02:00
c6b237be46 admin: add a route to simulate time.json on backend machine 2017-04-05 02:06:36 +02:00
1a8e7066c3 db: add constraints to avoid multiple records of unique values 2017-04-05 02:06:36 +02:00
4793d0de4e admin: add button and route to reset some parts 2017-04-05 02:06:35 +02:00
89eaeef88e admin: interface to edit teams 2017-04-05 02:06:35 +02:00
012be2c69a frontend: improve 401 page thank to initial guide 2017-04-05 02:06:34 +02:00
b2ce6dfbdf backend: generate an event when a team open an hint 2017-04-05 02:06:34 +02:00
1c6a665a98 frontend: move file (on the same partition) instead of open, write, close the final file 2017-04-05 02:06:33 +02:00
a449131fa8 libfic: new function to retrieve exercices from a hint 2017-04-05 02:06:33 +02:00
610217f87c change the way themes are stored in stats 2017-04-05 02:06:32 +02:00
fa8c9caaf0 admin: can force page regeneration 2017-04-05 02:06:32 +02:00
95d60c69e0 Update openssl settings 2017-04-05 02:06:31 +02:00
37b3974d69 admin: new route /members/ 2017-04-05 02:06:30 +02:00
725e867702 admin: add public interface management 2017-04-05 02:06:30 +02:00
14277e525f public interface: rework 2017-04-05 02:06:29 +02:00
4bce3aa1c1 admin: allow import of remote hint and partials remote parts 2017-04-05 02:06:29 +02:00
dd4e207892 admin: restore function to add team and members 2017-04-05 02:06:28 +02:00
9bf91f819b admin: sanitize use of InitialName when needed 2017-04-05 02:06:28 +02:00
cbfea4444e frontend: move time in a separate package to be used elsewhere 2017-04-05 02:06:27 +02:00
1376be011b certificates: avoid error on noexec partition 2017-04-05 02:06:26 +02:00
8d7a291fd4 admin: Display time before start in UI 2017-04-05 02:06:26 +02:00
e3673c6b18 backend: don't regenerate files if config doesn't change 2017-04-05 02:06:25 +02:00
25e1dc5065 Force cd into PKI directory 2017-04-05 02:06:25 +02:00
edcb9a5256 frontend: fix partial solved flags display 2017-04-05 02:06:24 +02:00
0b965a36e3 settings: admin interface see default params 2017-04-05 02:06:23 +02:00
61bde6d31e admin: control settings 2017-04-05 02:06:23 +02:00
eab92973e8 Coefficients transit and display on UI 2017-04-05 02:06:22 +02:00
e428a43109 fixup! fixup! WIP esthetic changes 2017-04-05 02:06:22 +02:00
b7b56a0628 frontend: dedicate a field in JSON to file hint 2017-04-05 02:06:21 +02:00
c2dea2f985 Hints can something else than text 2017-04-05 02:06:21 +02:00
ee8bb97057 front: use ng-pluralize 2017-04-05 02:06:20 +02:00
c8297237ad WIP esthetic changes 2017-04-05 02:06:20 +02:00
d106a4766d libfic: refactor rank/points SQL query 2017-04-05 02:06:19 +02:00
2254ee7702 admin: Improve CA API 2017-04-05 02:06:18 +02:00
6d5ded2c3b squash! WIP: apply a coeff on given points 2017-04-05 02:06:18 +02:00
b30f3b18e6 frontend: improve rank rendering 2017-04-05 02:06:17 +02:00
e41b3acb7e fill_exercices: flags.txt files can use tabulation char as separator instead of : 2017-04-05 02:06:16 +02:00
f14a7940b3 frontend: use a common JS file to contain common features between challenger and public interface 2017-04-05 02:06:16 +02:00
cbb58bcefb WIP: apply a coeff on given points 2017-04-05 02:06:15 +02:00
7d26b172ea frontend: add /rules page 2017-04-05 02:06:15 +02:00
fae97e5411 Settings are now given through TEAMS/settings.json instead of been given through command line arguments 2017-04-05 02:06:14 +02:00
b1541d9a45 New rank and score calculation 2017-04-05 02:06:14 +02:00
8108125cb8 backend: log generation errors 2017-04-05 02:06:13 +02:00
56d43cc65b fill_exercice: define HINT_COST 2017-04-05 02:06:12 +02:00
d7d22fe471 Handle file import digest 2017-04-05 02:06:11 +02:00
65d40773cc admin: various fixes in fill_exercices 2017-04-05 02:06:11 +02:00
4ff0c0ac59 admin: can pass args to fill_exercices to limit the fill to a theme or an exercice 2017-04-05 02:06:10 +02:00
4cea4a4aa0 admin: new argument --rapidimport to speed up the import but don't ensure consistency 2017-04-05 02:06:10 +02:00
3636002549 Split team.go into multiple files 2017-04-05 02:06:09 +02:00
ca266c1709 [admin] Add new routes to manage hints, files and keys 2017-04-05 02:06:08 +02:00
f14e9e80c8 [admin] Add events 2017-04-05 02:06:08 +02:00
46d452c82b [admin] Add exercices related pages 2017-04-05 02:06:07 +02:00
c23a71912b [admin] Add page title 2017-04-05 02:06:07 +02:00
632e699fa8 [admin] Add ng-sanitize 2017-04-05 02:06:06 +02:00
5111143d2a Merge exercices API routes 2017-04-05 02:06:05 +02:00
f008aac04c Bump new version API 2017-04-05 02:06:05 +02:00
1bb978a9c6 Use github.com/julienschmidt/httprouter instead of gorilla 2017-04-05 02:06:04 +02:00
0d6e36798c Merge big splitted files before import 2017-04-05 02:06:04 +02:00
00dfbd92dd Use 2017 logos 2017-04-05 02:06:03 +02:00
937990fb48 frontend: interface can open hints 2017-04-05 02:06:02 +02:00
3d60896bdf frontend: able to receive opening hint 2017-04-05 02:06:01 +02:00
c669319e56 backend: can open hint 2017-04-05 02:06:01 +02:00
6e7e174713 frontend: refactor and dispatch in many routes 2017-04-05 02:06:00 +02:00
4fc4e34a4e WIP misc 2017-04-05 02:05:59 +02:00
ee5335e515 Partial resolution of exercices 2017-04-05 02:05:59 +02:00
ccbc787001 Multiple hints 2017-04-05 02:05:58 +02:00
9b35e78163 backend: use fsnotify instead of the deprecated inotify 2017-04-05 02:05:58 +02:00
10dc6c4d30 admin/api: use gorilla/mux instead of Go router 2017-04-05 02:05:57 +02:00
27ef7cb6c1 frontend: redesign download part 2017-04-05 02:05:57 +02:00
fa98b4bde3 frontend: add some glyphicons 2017-04-05 02:05:56 +02:00
8fab3aa85d frontend: move user box to the top of the page 2017-04-05 02:05:56 +02:00
003ceb8f98 backend: new option that unlock all challenges 2017-04-05 02:05:55 +02:00
07c1a22d75 themes: don't expect authors to be dirty 2017-04-05 02:05:55 +02:00
e04f94efbf frontend: fail if TEAMS directory doesn't exists 2017-04-05 02:05:54 +02:00
28054a3dd7 frontend: add resolution route 2017-04-05 02:05:53 +02:00
cbc0ad6a8d frontend: add link to frontend htdocs, like admin static pages 2017-04-05 02:05:53 +02:00
7bb7da5338 admin: can give the static dir location 2017-04-05 01:51:24 +02:00
7fb4b22a1f admin: can change the baseurl interface 2017-04-05 01:51:23 +02:00
ab4bf8f307 by default, only listen on localhost 2017-04-05 01:51:22 +02:00
a9da1fe059 fill_team: improve script reliability 2017-04-05 01:51:22 +02:00
1ccec4ab29 admin: add ability to add files from local storage 2017-04-05 01:51:21 +02:00
102 changed files with 5160 additions and 2116 deletions

View file

@ -1,102 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
)
type DispatchFunction func([]string, []byte) (interface{}, error)
var apiRoutes = map[string]*(map[string]DispatchFunction){
"version": &ApiVersionRouting,
"ca": &ApiCARouting,
"events": &ApiEventsRouting,
"exercices": &ApiExercicesRouting,
"themes": &ApiThemesRouting,
"teams": &ApiTeamsRouting,
}
type apiRouting struct{}
func ApiHandler() http.Handler {
return apiRouting{}
}
func (a apiRouting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
// Extract URL arguments
var sURL = strings.Split(r.URL.Path, "/")[1:]
if len(sURL) > 1 && sURL[len(sURL)-1] == "" {
// Remove trailing /
sURL = sURL[:len(sURL)-1]
}
w.Header().Set("Content-Type", "application/json")
var ret interface{}
var err error = nil
// Read the body
if r.ContentLength < 0 || r.ContentLength > 6553600 {
http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}", err), http.StatusRequestEntityTooLarge)
return
}
var body []byte
if r.ContentLength > 0 {
tmp := make([]byte, 1024)
for {
n, err := r.Body.Read(tmp)
for j := 0; j < n; j++ {
body = append(body, tmp[j])
}
if err != nil || n <= 0 {
break
}
}
}
// Route request
if len(sURL) > 0 {
if h, ok := apiRoutes[sURL[0]]; ok {
if f, ok := (*h)[r.Method]; ok {
ret, err = f(sURL[1:], body)
} else {
err = errors.New(fmt.Sprintf("Invalid action (%s) provided for %s.", r.Method, sURL[0]))
}
}
} else {
err = errors.New("No action provided.")
}
// Format response
resStatus := http.StatusOK
if err != nil {
ret = map[string]string{"errmsg": err.Error()}
resStatus = http.StatusBadRequest
log.Println(r.RemoteAddr, resStatus, err.Error())
}
if ret == nil {
ret = map[string]string{"errmsg": "Page not found"}
resStatus = http.StatusNotFound
}
if str, found := ret.(string); found {
w.WriteHeader(resStatus)
io.WriteString(w, str)
} else if bts, found := ret.([]byte); found {
w.WriteHeader(resStatus)
w.Write(bts)
} else if j, err := json.Marshal(ret); err != nil {
http.Error(w, fmt.Sprintf("{\"errmsg\":\"%q\"}", err), http.StatusInternalServerError)
} else {
w.WriteHeader(resStatus)
w.Write(j)
}
}

60
admin/api/certificate.go Normal file
View file

@ -0,0 +1,60 @@
package api
import (
"errors"
"io/ioutil"
"os"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/ca.pem", apiHandler(GetCAPEM))
router.POST("/api/ca/new", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.GenerateCA() }))
router.GET("/api/ca/crl", apiHandler(GetCRL))
router.POST("/api/ca/crl", apiHandler(
func(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.GenerateCRL() }))
router.HEAD("/api/teams/:tid/certificate.p12", apiHandler(teamHandler(GetTeamCertificate)))
router.GET("/api/teams/:tid/certificate.p12", apiHandler(teamHandler(GetTeamCertificate)))
router.DELETE("/api/teams/:tid/certificate.p12", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) { return team.RevokeCert() })))
router.GET("/api/teams/:tid/certificate/generate", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) { return team.GenerateCert() })))
}
func GetCAPEM(_ httprouter.Params, _ []byte) (interface{}, error) {
if _, err := os.Stat("../PKI/shared/cacert.crt"); os.IsNotExist(err) {
return nil, errors.New("Unable to locate the CA root certificate. Have you generated it?")
} else if fd, err := os.Open("../PKI/shared/cacert.crt"); err == nil {
return ioutil.ReadAll(fd)
} else {
return nil, err
}
}
func GetCRL(_ httprouter.Params, _ []byte) (interface{}, error) {
if _, err := os.Stat("../PKI/shared/crl.pem"); os.IsNotExist(err) {
return nil, errors.New("Unable to locate the CRL. Have you generated it?")
} else if fd, err := os.Open("../PKI/shared/crl.pem"); err == nil {
return ioutil.ReadAll(fd)
} else {
return nil, err
}
}
func GetTeamCertificate(team fic.Team, _ []byte) (interface{}, error) {
if _, err := os.Stat("../PKI/pkcs/" + team.InitialName + ".p12"); os.IsNotExist(err) {
return nil, errors.New("Unable to locate the p12. Have you generated it?")
} else if fd, err := os.Open("../PKI/pkcs/" + team.InitialName + ".p12"); err == nil {
return ioutil.ReadAll(fd)
} else {
return nil, err
}
}

76
admin/api/events.go Normal file
View file

@ -0,0 +1,76 @@
package api
import (
"encoding/json"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/events/", apiHandler(getEvents))
router.GET("/api/events.json", apiHandler(getLastEvents))
router.POST("/api/events/", apiHandler(newEvent))
router.DELETE("/api/events/", apiHandler(clearEvents))
router.GET("/api/events/:evid", apiHandler(eventHandler(showEvent)))
router.PUT("/api/events/:evid", apiHandler(eventHandler(updateEvent)))
router.DELETE("/api/events/:evid", apiHandler(eventHandler(deleteEvent)))
}
func getEvents(_ httprouter.Params, _ []byte) (interface{}, error) {
if evts, err := fic.GetEvents(); err != nil {
return nil, err
} else {
return evts, nil
}
}
func getLastEvents(_ httprouter.Params, _ []byte) (interface{}, error) {
if evts, err := fic.GetLastEvents(); err != nil {
return nil, err
} else {
return evts, nil
}
}
func showEvent(event fic.Event, _ []byte) (interface{}, error) {
return event, nil
}
func newEvent(_ httprouter.Params, body []byte) (interface{}, error) {
var ue fic.Event
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if event, err := fic.NewEvent(ue.Text, ue.Kind); err != nil {
return nil, err
} else {
return event, nil
}
}
func clearEvents(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.ClearEvents()
}
func updateEvent(event fic.Event, body []byte) (interface{}, error) {
var ue fic.Event
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
ue.Id = event.Id
if _, err := ue.Update(); err != nil {
return nil, err
} else {
return ue, nil
}
}
func deleteEvent(event fic.Event, _ []byte) (interface{}, error) {
return event.Delete()
}

221
admin/api/exercice.go Normal file
View file

@ -0,0 +1,221 @@
package api
import (
"encoding/json"
"errors"
"strings"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/exercices/", apiHandler(listExercices))
router.GET("/api/exercices/:eid", apiHandler(exerciceHandler(showExercice)))
router.PUT("/api/exercices/:eid", apiHandler(exerciceHandler(updateExercice)))
router.DELETE("/api/exercices/:eid", apiHandler(exerciceHandler(deleteExercice)))
router.GET("/api/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
router.POST("/api/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
router.GET("/api/exercices/:eid/files/:fid", apiHandler(fileHandler(showExerciceFile)))
router.DELETE("/api/exercices/:eid/files/:fid", apiHandler(fileHandler(deleteExerciceFile)))
router.GET("/api/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints)))
router.POST("/api/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint)))
router.GET("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(showExerciceHint)))
router.PUT("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(updateExerciceHint)))
router.DELETE("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(deleteExerciceHint)))
router.GET("/api/exercices/:eid/keys", apiHandler(exerciceHandler(listExerciceKeys)))
router.POST("/api/exercices/:eid/keys", apiHandler(exerciceHandler(createExerciceKey)))
router.GET("/api/exercices/:eid/keys/:kid", apiHandler(keyHandler(showExerciceKey)))
router.PUT("/api/exercices/:eid/keys/:kid", apiHandler(keyHandler(updateExerciceKey)))
router.DELETE("/api/exercices/:eid/keys/:kid", apiHandler(keyHandler(deleteExerciceKey)))
}
func listExercices(_ httprouter.Params, body []byte) (interface{}, error) {
// List all exercices
return fic.GetExercices()
}
func listExerciceFiles(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetFiles()
}
func listExerciceHints(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetHints()
}
func listExerciceKeys(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetKeys()
}
func showExercice(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice, nil
}
func deleteExercice(exercice fic.Exercice, _ []byte) (interface{}, error) {
return exercice.Delete()
}
func updateExercice(exercice fic.Exercice, body []byte) (interface{}, error) {
var ue fic.Exercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
ue.Id = exercice.Id
if len(ue.Title) == 0 {
return nil, errors.New("Exercice's title not filled")
}
if _, err := ue.Update(); err != nil {
return nil, err
}
return ue, nil
}
func createExercice(theme fic.Theme, body []byte) (interface{}, error) {
// Create a new exercice
var ue fic.Exercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Title not filled")
}
var depend *fic.Exercice = nil
if ue.Depend != nil {
if d, err := fic.GetExercice(*ue.Depend); err != nil {
return nil, err
} else {
depend = &d
}
}
return theme.AddExercice(ue.Title, ue.Statement, depend, ue.Gain, ue.VideoURI)
}
type uploadedKey struct {
Type string
Key string
}
func createExerciceKey(exercice fic.Exercice, body []byte) (interface{}, error) {
var uk uploadedKey
if err := json.Unmarshal(body, &uk); err != nil {
return nil, err
}
if len(uk.Key) == 0 {
return nil, errors.New("Key not filled")
}
return exercice.AddRawKey(uk.Type, uk.Key)
}
type uploadedHint struct {
Title string
Content string
Cost int64
URI string
Path string
}
func createExerciceHint(exercice fic.Exercice, body []byte) (interface{}, error) {
var uh uploadedHint
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
}
if len(uh.Content) != 0 {
return exercice.AddHint(uh.Title, uh.Content, uh.Cost)
} else if len(uh.Path) != 0 || len(uh.URI) != 0 {
return importFile(uploadedFile{Path: uh.Path, URI: uh.URI},
func(filePath string, origin string, digest []byte) (interface{}, error) {
return exercice.AddHint(uh.Title, "$FILES" + strings.TrimPrefix(filePath, fic.FilesDir), uh.Cost)
})
} else {
return nil, errors.New("Hint's content not filled")
}
}
func showExerciceHint(hint fic.EHint, body []byte) (interface{}, error) {
return hint, nil
}
func updateExerciceHint(hint fic.EHint, body []byte) (interface{}, error) {
var uh fic.EHint
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
}
uh.Id = hint.Id
if len(uh.Title) == 0 {
return nil, errors.New("Hint's title not filled")
}
if _, err := uh.Update(); err != nil {
return nil, err
}
return uh, nil
}
func deleteExerciceHint(hint fic.EHint, _ []byte) (interface{}, error) {
return hint.Delete()
}
func showExerciceKey(key fic.Key, _ fic.Exercice, body []byte) (interface{}, error) {
return key, nil
}
func updateExerciceKey(key fic.Key, exercice fic.Exercice, body []byte) (interface{}, error) {
var uk fic.Key
if err := json.Unmarshal(body, &uk); err != nil {
return nil, err
}
uk.Id = key.Id
uk.IdExercice = exercice.Id
if len(uk.Type) == 0 {
uk.Type = "Flag"
}
if _, err := uk.Update(); err != nil {
return nil, err
}
return uk, nil
}
func deleteExerciceKey(key fic.Key, _ fic.Exercice, _ []byte) (interface{}, error) {
return key.Delete()
}
func createExerciceFile(exercice fic.Exercice, body []byte) (interface{}, error) {
var uf uploadedFile
if err := json.Unmarshal(body, &uf); err != nil {
return nil, err
}
return importFile(uf, exercice.ImportFile)
}
func showExerciceFile(file fic.EFile, body []byte) (interface{}, error) {
return file, nil
}
func deleteExerciceFile(file fic.EFile, _ []byte) (interface{}, error) {
return file.Delete()
}

153
admin/api/file.go Normal file
View file

@ -0,0 +1,153 @@
package api
import (
"bufio"
"crypto/sha512"
"encoding/base32"
"encoding/hex"
"errors"
"fmt"
"log"
"net/http"
"os"
"path"
"strings"
"srs.epita.fr/fic-server/libfic"
)
var CloudDAVBase string
var CloudUsername string
var CloudPassword string
var RapidImport bool
type uploadedFile struct {
URI string
Digest string
Path string
Parts []string
}
func importFile(uf uploadedFile, next func(string, string, []byte) (interface{}, error)) (interface{}, error) {
var hash [sha512.Size]byte
var logStr string
var fromURI string
var getFile func(string) (error)
if uf.URI != "" && len(uf.Parts) > 0 {
hash = sha512.Sum512([]byte(uf.URI))
logStr = fmt.Sprintf("Import file from Cloud: %s =>", uf.Parts)
fromURI = uf.URI
getFile = func(dest string) error {
if fdto, err := os.Create(dest); err != nil {
return err
} else {
writer := bufio.NewWriter(fdto)
for _, partname := range uf.Parts {
if err := getCloudPart(partname, writer); err != nil {
return err
}
}
fdto.Close()
}
return nil
}
} else if uf.URI != "" {
hash = sha512.Sum512([]byte(uf.URI))
logStr = "Import file from Cloud: " + uf.URI + " =>"
fromURI = uf.URI
getFile = func(dest string) error { return getCloudFile(uf.URI, dest); }
} else if uf.Path != "" && len(uf.Parts) > 0 {
hash = sha512.Sum512([]byte(uf.Path))
logStr = fmt.Sprintf("Import file from local FS: %s =>", uf.Parts)
fromURI = uf.Path
getFile = func(dest string) error {
if fdto, err := os.Create(dest); err != nil {
return err
} else {
writer := bufio.NewWriter(fdto)
for _, partname := range uf.Parts {
if fdfrm, err := os.Open(partname); err != nil {
return err
} else {
reader := bufio.NewReader(fdfrm)
reader.WriteTo(writer)
writer.Flush()
fdfrm.Close()
}
}
fdto.Close()
}
return nil
}
} else if uf.Path != "" {
hash = sha512.Sum512([]byte(uf.Path))
logStr = "Import file from local FS: " + uf.Path + " =>"
fromURI = uf.Path
getFile = func(dest string) error { return os.Symlink(uf.Path, dest); }
} else {
return nil, errors.New("URI or path not filled")
}
pathname := path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.EncodeToString(hash[:])), path.Base(fromURI))
// Remove the file if it exists
// TODO: check if this is symlink => remove to avoid File not found error after, because the file is writen at the adresse pointed.
if _, err := os.Stat(pathname); !os.IsNotExist(err) && !RapidImport {
if err := os.Remove(pathname); err != nil {
return nil, err
}
}
if _, err := os.Stat(pathname); os.IsNotExist(err) {
log.Println(logStr, pathname)
if err := os.MkdirAll(path.Dir(pathname), 0777); err != nil {
return nil, err
} else if err := getFile(pathname); err != nil {
return nil, err
}
}
if digest, err := hex.DecodeString(uf.Digest); err != nil {
return nil, err
} else {
return next(pathname, fromURI, digest)
}
}
func getCloudFile(pathname string, dest string) error {
if fd, err := os.Create(dest); err != nil {
return err
} else {
defer fd.Close()
writer := bufio.NewWriter(fd)
if err := getCloudPart(pathname, writer); err != nil {
return err
}
}
return nil
}
func getCloudPart(pathname string, writer *bufio.Writer) error {
client := http.Client{}
if req, err := http.NewRequest("GET", CloudDAVBase+pathname, nil); err != nil {
return err
} else {
req.SetBasicAuth(CloudUsername, CloudPassword)
if resp, err := client.Do(req); err != nil {
return err
} else {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
} else {
reader := bufio.NewReader(resp.Body)
reader.WriteTo(writer)
writer.Flush()
}
}
}
return nil
}

227
admin/api/handlers.go Normal file
View file

@ -0,0 +1,227 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
type DispatchFunction func(httprouter.Params, []byte) (interface{}, error)
func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "application/json")
var ret interface{}
var err error = nil
// Read the body
if r.ContentLength < 0 || r.ContentLength > 6553600 {
http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}", err), http.StatusRequestEntityTooLarge)
return
}
var body []byte
if r.ContentLength > 0 {
tmp := make([]byte, 1024)
for {
n, err := r.Body.Read(tmp)
for j := 0; j < n; j++ {
body = append(body, tmp[j])
}
if err != nil || n <= 0 {
break
}
}
}
ret, err = f(ps, body)
// Format response
resStatus := http.StatusOK
if err != nil {
ret = map[string]string{"errmsg": err.Error()}
resStatus = http.StatusBadRequest
log.Println(r.RemoteAddr, resStatus, err.Error())
}
if ret == nil {
ret = map[string]string{"errmsg": "Page not found"}
resStatus = http.StatusNotFound
}
if str, found := ret.(string); found {
w.WriteHeader(resStatus)
io.WriteString(w, str)
} else if bts, found := ret.([]byte); found {
w.WriteHeader(resStatus)
w.Write(bts)
} else if j, err := json.Marshal(ret); err != nil {
http.Error(w, fmt.Sprintf("{\"errmsg\":\"%q\"}", err), http.StatusInternalServerError)
} else {
w.WriteHeader(resStatus)
w.Write(j)
}
}
}
func teamPublicHandler(f func(*fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
if tid, err := strconv.Atoi(string(ps.ByName("tid"))); err != nil {
if team, err := fic.GetTeamByInitialName(ps.ByName("tid")); err != nil {
return nil, err
} else {
return f(&team, body)
}
} else if tid == 0 {
return f(nil, body)
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
return f(&team, body)
}
}
}
func teamHandler(f func(fic.Team,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
if tid, err := strconv.Atoi(string(ps.ByName("tid"))); err != nil {
if team, err := fic.GetTeamByInitialName(ps.ByName("tid")); err != nil {
return nil, err
} else {
return f(team, body)
}
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
return f(team, body)
}
}
}
func themeHandler(f func(fic.Theme,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
if thid, err := strconv.Atoi(string(ps.ByName("thid"))); err != nil {
return nil, err
} else if theme, err := fic.GetTheme(thid); err != nil {
return nil, err
} else {
return f(theme, body)
}
}
}
func exerciceHandler(f func(fic.Exercice,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
if eid, err := strconv.Atoi(string(ps.ByName("eid"))); err != nil {
return nil, err
} else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
return nil, err
} else {
return f(exercice, body)
}
}
}
func themedExerciceHandler(f func(fic.Theme,fic.Exercice,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
var theme fic.Theme
var exercice fic.Exercice
themeHandler(func (th fic.Theme, _[]byte) (interface{}, error) {
theme = th
return nil,nil
})(ps, body)
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) {
exercice = ex
return nil,nil
})(ps, body)
return f(theme, exercice, body)
}
}
func hintHandler(f func(fic.EHint,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
if hid, err := strconv.Atoi(string(ps.ByName("hid"))); err != nil {
return nil, err
} else if hint, err := fic.GetHint(int64(hid)); err != nil {
return nil, err
} else {
return f(hint, body)
}
}
}
func keyHandler(f func(fic.Key,fic.Exercice,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
var exercice fic.Exercice
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) {
exercice = ex
return nil,nil
})(ps, body)
if kid, err := strconv.Atoi(string(ps.ByName("kid"))); err != nil {
return nil, err
} else if keys, err := exercice.GetKeys(); err != nil {
return nil, err
} else {
for _, key := range keys {
if (key.Id == int64(kid)) {
return f(key, exercice, body)
}
}
return nil, errors.New("Unable to find the requested key")
}
}
}
func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
var exercice fic.Exercice
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) {
exercice = ex
return nil,nil
})(ps, body)
if fid, err := strconv.Atoi(string(ps.ByName("fid"))); err != nil {
return nil, err
} else if files, err := exercice.GetFiles(); err != nil {
return nil, err
} else {
for _, file := range files {
if (file.Id == int64(fid)) {
return f(file, body)
}
}
return nil, errors.New("Unable to find the requested file")
}
}
}
func eventHandler(f func(fic.Event,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
return func (ps httprouter.Params, body []byte) (interface{}, error) {
if evid, err := strconv.Atoi(string(ps.ByName("evid"))); err != nil {
return nil, err
} else if event, err := fic.GetEvent(evid); err != nil {
return nil, err
} else {
return f(event, body)
}
}
}
func notFound(ps httprouter.Params, _ []byte) (interface{}, error) {
return nil, nil
}

80
admin/api/public.go Normal file
View file

@ -0,0 +1,80 @@
package api
import (
"encoding/json"
"os"
"path"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/public.json", apiHandler(getPublic))
router.DELETE("/api/public.json", apiHandler(deletePublic))
router.PUT("/api/public.json", apiHandler(savePublic))
}
type FICPublicScene struct {
Type string `json:"type"`
Params map[string]interface{} `json:"params"`
}
func readPublic(path string) ([]FICPublicScene, error) {
var s []FICPublicScene
if fd, err := os.Open(path); err != nil {
return s, err
} else {
defer fd.Close()
jdec := json.NewDecoder(fd)
if err := jdec.Decode(&s); err != nil {
return s, err
}
return s, nil
}
}
func savePublicTo(path string, s []FICPublicScene) error {
if fd, err := os.Create(path); err != nil {
return err
} else {
defer fd.Close()
jenc := json.NewEncoder(fd)
if err := jenc.Encode(s); err != nil {
return err
}
return nil
}
}
func getPublic(_ httprouter.Params, body []byte) (interface{}, error) {
if _, err := os.Stat(path.Join(TeamsDir, "_public", "public.json")); !os.IsNotExist(err) {
return readPublic(path.Join(TeamsDir, "_public", "public.json"))
} else {
return []FICPublicScene{}, nil
}
}
func deletePublic(_ httprouter.Params, body []byte) (interface{}, error) {
if err := savePublicTo(path.Join(TeamsDir, "_public", "public.json"), []FICPublicScene{}); err != nil {
return nil, err
} else {
return []FICPublicScene{}, err
}
}
func savePublic(_ httprouter.Params, body []byte) (interface{}, error) {
var scenes []FICPublicScene
if err := json.Unmarshal(body, &scenes); err != nil {
return nil, err
}
if err := savePublicTo(path.Join(TeamsDir, "_public", "public.json"), scenes); err != nil {
return nil, err
} else {
return scenes, err
}
}

11
admin/api/router.go Normal file
View file

@ -0,0 +1,11 @@
package api
import (
"github.com/julienschmidt/httprouter"
)
var router = httprouter.New()
func Router() *httprouter.Router {
return router
}

62
admin/api/settings.go Normal file
View file

@ -0,0 +1,62 @@
package api
import (
"encoding/json"
"errors"
"path"
"time"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/julienschmidt/httprouter"
)
var TeamsDir string
func init() {
router.GET("/api/settings.json", apiHandler(getSettings))
router.PUT("/api/settings.json", apiHandler(saveSettings))
router.POST("/api/reset", apiHandler(reset))
}
func getSettings(_ httprouter.Params, body []byte) (interface{}, error) {
if settings.ExistsSettings(path.Join(TeamsDir, settings.SettingsFile)) {
return settings.ReadSettings(path.Join(TeamsDir, settings.SettingsFile))
} else {
return settings.FICSettings{"Challenge FIC", "Laboratoire SRS, ÉPITA", time.Unix(0,0), time.Unix(0,0), time.Unix(0,0), fic.FirstBlood, fic.SubmissionCostBase, false, false, false, true, true}, nil
}
}
func saveSettings(_ httprouter.Params, body []byte) (interface{}, error) {
var config settings.FICSettings
if err := json.Unmarshal(body, &config); err != nil {
return nil, err
}
if err := settings.SaveSettings(path.Join(TeamsDir, settings.SettingsFile), config); err != nil {
return nil, err
} else {
return config, err
}
}
func reset(_ httprouter.Params, body []byte) (interface{}, error) {
var m map[string]string
if err := json.Unmarshal(body, &m); err != nil {
return nil, err
}
if t, ok := m["type"]; !ok {
return nil, errors.New("Field type not found")
} else if t == "teams" {
return true, fic.ResetTeams()
} else if t == "challenges" {
return true, fic.ResetExercices()
} else if t == "game" {
return true, fic.ResetGame()
} else {
return nil, errors.New("Unknown reset type")
}
}

View file

@ -1,4 +1,4 @@
package main
package api
import (
"fmt"
@ -30,6 +30,7 @@ func genStats() (interface{}, error) {
exos[fmt.Sprintf("%d", exercice.Id)] = fic.ExportedExercice{
exercice.Title,
exercice.Gain,
exercice.Coefficient,
exercice.SolvedCount(),
exercice.TriedTeamCount(),
}

223
admin/api/team.go Normal file
View file

@ -0,0 +1,223 @@
package api
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/teams.json", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) {
return fic.ExportTeams() }))
router.GET("/api/teams-stats.json", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := map[int64]interface{}{}
if stats, err := fic.GetTeamsStats(nil); err != nil {
return ret, err
} else {
ret[0] = stats
}
for _, team := range teams {
if stats, err := team.GetStats(); err != nil {
return ret, err
} else {
ret[team.Id] = stats
}
}
return ret, nil
}
}))
router.GET("/api/teams-binding", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) {
return bindingTeams() }))
router.GET("/api/teams-nginx", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) {
return nginxGenTeam() }))
router.GET("/api/teams-nginx-members", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) {
return nginxGenMember() }))
router.GET("/api/teams/", apiHandler(
func(httprouter.Params,[]byte) (interface{}, error) {
return fic.GetTeams() }))
router.POST("/api/teams/", apiHandler(createTeam))
router.GET("/api/teams/:tid/", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) {
return team, nil })))
router.PUT("/api/teams/:tid/", apiHandler(teamHandler(updateTeam)))
router.POST("/api/teams/:tid/", apiHandler(teamHandler(addTeamMember)))
router.DELETE("/api/teams/:tid/", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) {
return team.Delete() })))
router.GET("/api/teams/:tid/my.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.MyJSONTeam(team, true) })))
router.GET("/api/teams/:tid/wait.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.MyJSONTeam(team, false) })))
router.GET("/api/teams/:tid/stats.json", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
if team != nil {
return team.GetStats()
} else {
return fic.GetTeamsStats(nil)
}
})))
router.GET("/api/teams/:tid/tries", apiHandler(teamPublicHandler(
func(team *fic.Team, _ []byte) (interface{}, error) {
return fic.GetTries(team, nil) })))
router.GET("/api/teams/:tid/members", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) {
return team.GetMembers() })))
router.POST("/api/teams/:tid/members", apiHandler(teamHandler(addTeamMember)))
router.PUT("/api/teams/:tid/members", apiHandler(teamHandler(setTeamMember)))
router.GET("/api/teams/:tid/name", apiHandler(teamHandler(
func(team fic.Team, _ []byte) (interface{}, error) {
return team.InitialName, nil })))
router.GET("/api/members/:mid/team", apiHandler(dispMemberTeam))
router.GET("/api/members/:mid/team/name", apiHandler(dispMemberTeamName))
}
func nginxGenMember() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err == nil {
for _, member := range members {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%s\"; }\n", member.Nickname, team.InitialName)
}
} else {
return "", err
}
}
return ret, nil
}
}
func nginxGenTeam() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
ret += fmt.Sprintf(" if ($ssl_client_s_dn ~ \"/C=FR/ST=France/O=Epita/OU=SRS/CN=%s\") { set $team \"%s\"; }\n", team.InitialName, team.InitialName)
}
return ret, nil
}
}
func bindingTeams() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err != nil {
return "", err
} else {
var mbs []string
for _, member := range members {
mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname))
}
ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
}
}
return ret, nil
}
}
type uploadedTeam struct {
Name string
Color uint32
}
type uploadedMember struct {
Firstname string
Lastname string
Nickname string
Company string
}
func createTeam(_ httprouter.Params, body []byte) (interface{}, error) {
var ut uploadedTeam
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
return fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color)
}
func updateTeam(team fic.Team, body []byte) (interface{}, error) {
var ut fic.Team
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
ut.Id = team.Id
if _, err := ut.Update(); err != nil {
return nil, err
}
return ut, nil
}
func addTeamMember(team fic.Team, body []byte) (interface{}, error) {
var members []uploadedMember
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
}
for _, member := range members {
team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
}
return team.GetMembers()
}
func setTeamMember(team fic.Team, body []byte) (interface{}, error) {
var members []uploadedMember
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
}
team.ClearMembers()
for _, member := range members {
team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
}
return team.GetMembers()
}
func dispMemberTeam(ps httprouter.Params, body []byte) (interface{}, error) {
if mid, err := strconv.Atoi(string(ps.ByName("mid"))); err != nil {
return fic.Team{}, err
} else {
return fic.GetMember(mid)
}
}
func dispMemberTeamName(ps httprouter.Params, body []byte) (interface{}, error) {
if mid, err := strconv.Atoi(string(ps.ByName("mid"))); err != nil {
return nil, err
} else if team, err := fic.GetMember(mid); err != nil {
return nil, err
} else {
return team.InitialName, nil
}
}

126
admin/api/theme.go Normal file
View file

@ -0,0 +1,126 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/themes", apiHandler(listThemes))
router.POST("/api/themes", apiHandler(createTheme))
router.GET("/api/themes.json", apiHandler(exportThemes))
router.GET("/api/files-bindings", apiHandler(bindingFiles))
router.GET("/api/themes/:thid", apiHandler(themeHandler(showTheme)))
router.PUT("/api/themes/:thid", apiHandler(themeHandler(updateTheme)))
router.DELETE("/api/themes/:thid", apiHandler(themeHandler(deleteTheme)))
router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices)))
router.POST("/api/themes/:thid/exercices", apiHandler(themeHandler(createExercice)))
router.GET("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(showExercice)))
router.PUT("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(updateExercice)))
router.DELETE("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(deleteExercice)))
router.GET("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
router.POST("/api/themes/:thid/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
router.GET("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints)))
router.POST("/api/themes/:thid/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint)))
router.GET("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(listExerciceKeys)))
router.POST("/api/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler(createExerciceKey)))
}
func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) {
if files, err := fic.GetFiles(); err != nil {
return "", err
} else {
ret := ""
for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
}
return ret, nil
}
}
func getExercice(args []string) (fic.Exercice, error) {
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return fic.Exercice{}, err
} else if theme, err := fic.GetTheme(tid); err != nil {
return fic.Exercice{}, err
} else if eid, err := strconv.Atoi(string(args[1])); err != nil {
return fic.Exercice{}, err
} else {
return theme.GetExercice(eid)
}
}
func listThemes(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.GetThemes()
}
func exportThemes(_ httprouter.Params, _ []byte) (interface{}, error) {
return fic.ExportThemes()
}
func showTheme(theme fic.Theme, _ []byte) (interface{}, error) {
return theme, nil
}
func listThemedExercices(theme fic.Theme, _ []byte) (interface{}, error) {
return theme.GetExercices()
}
func showThemedExercice(theme fic.Theme, exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice, nil
}
type uploadedTheme struct {
Name string
Authors string
}
func createTheme(_ httprouter.Params, body []byte) (interface{}, error) {
var ut uploadedTheme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
}
return fic.CreateTheme(ut.Name, ut.Authors)
}
func updateTheme(theme fic.Theme, body []byte) (interface{}, error) {
var ut fic.Theme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
ut.Id = theme.Id
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
}
if _, err := ut.Update(); err != nil {
return nil, err
} else {
return ut, nil
}
}
func deleteTheme(theme fic.Theme, _ []byte) (interface{}, error) {
return theme.Delete()
}

13
admin/api/version.go Normal file
View file

@ -0,0 +1,13 @@
package api
import (
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/version", apiHandler(showVersion))
}
func showVersion(_ httprouter.Params, body []byte) (interface{}, error) {
return map[string]interface{}{"version": 0.2}, nil
}

View file

@ -1,32 +0,0 @@
package main
import (
"io/ioutil"
"os"
"srs.epita.fr/fic-server/libfic"
)
func CertificateAPI(team fic.Team, args []string) (interface{}, error) {
if len(args) == 1 {
if args[0] == "generate" {
return team.GenerateCert(), nil
} else if args[0] == "revoke" {
return team.RevokeCert(), nil
} else {
return nil, nil
}
} else if fd, err := os.Open("../PKI/pkcs/" + team.Name + ".p12"); err == nil {
return ioutil.ReadAll(fd)
} else {
return nil, err
}
}
var ApiCARouting = map[string]DispatchFunction{
"GET": genCA,
}
func genCA(args []string, body []byte) (interface{}, error) {
return fic.GenerateCA(), nil
}

View file

@ -1,17 +0,0 @@
package main
import (
"srs.epita.fr/fic-server/libfic"
)
var ApiEventsRouting = map[string]DispatchFunction{
"GET": getEvents,
}
func getEvents(args []string, body []byte) (interface{}, error) {
if evts, err := fic.GetEvents(); err != nil {
return nil, err
} else {
return evts, nil
}
}

View file

@ -1,144 +0,0 @@
package main
import (
"encoding/json"
"errors"
"strconv"
"srs.epita.fr/fic-server/libfic"
)
var ApiExercicesRouting = map[string]DispatchFunction{
"GET": listExercice,
"PATCH": updateExercice,
"DELETE": deletionExercice,
}
func listExercice(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if eid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else {
return fic.GetExercice(int64(eid))
}
} else {
// List all exercices
return fic.GetExercices()
}
}
func deletionExercice(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if eid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
return nil, err
} else {
return exercice.Delete()
}
} else {
return nil, nil
}
}
type uploadedExercice struct {
Title string
Statement string
Hint string
Depend *int64
Gain int
VideoURI string
}
func updateExercice(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if eid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
return nil, err
} else {
// Update an exercice
var ue uploadedExercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Exercice's title not filled")
}
if ue.Depend != nil {
if _, err := fic.GetExercice(*ue.Depend); err != nil {
return nil, err
}
}
exercice.Title = ue.Title
exercice.Statement = ue.Statement
exercice.Hint = ue.Hint
exercice.Depend = ue.Depend
exercice.Gain = int64(ue.Gain)
exercice.VideoURI = ue.VideoURI
return exercice.Update()
}
} else {
return nil, nil
}
}
func createExercice(theme fic.Theme, args []string, body []byte) (interface{}, error) {
if len(args) >= 1 {
if eid, err := strconv.Atoi(args[0]); err != nil {
return nil, err
} else if exercice, err := theme.GetExercice(eid); err != nil {
return nil, err
} else {
if args[1] == "files" {
return createExerciceFile(theme, exercice, args[2:], body)
} else if args[1] == "keys" {
return createExerciceKey(theme, exercice, args[2:], body)
}
}
return nil, nil
} else {
// Create a new exercice
var ue uploadedExercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Title not filled")
}
var depend *fic.Exercice = nil
if ue.Depend != nil {
if d, err := fic.GetExercice(*ue.Depend); err != nil {
return nil, err
} else {
depend = &d
}
}
return theme.AddExercice(ue.Title, ue.Statement, ue.Hint, depend, ue.Gain, ue.VideoURI)
}
}
type uploadedKey struct {
Name string
Key string
}
func createExerciceKey(theme fic.Theme, exercice fic.Exercice, args []string, body []byte) (interface{}, error) {
var uk uploadedKey
if err := json.Unmarshal(body, &uk); err != nil {
return nil, err
}
if len(uk.Key) == 0 {
return nil, errors.New("Key not filled")
}
return exercice.AddRawKey(uk.Name, uk.Key)
}

View file

@ -1,75 +0,0 @@
package main
import (
"bufio"
"crypto/sha512"
"encoding/base32"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"path"
"strings"
"srs.epita.fr/fic-server/libfic"
)
type uploadedFile struct {
URI string
}
func createExerciceFile(theme fic.Theme, exercice fic.Exercice, args []string, body []byte) (interface{}, error) {
var uf uploadedFile
if err := json.Unmarshal(body, &uf); err != nil {
return nil, err
}
if len(uf.URI) == 0 {
return nil, errors.New("URI not filled")
}
hash := sha512.Sum512([]byte(uf.URI))
pathname := path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.EncodeToString(hash[:])), path.Base(uf.URI))
if _, err := os.Stat(pathname); os.IsNotExist(err) {
log.Println("Import file from Cloud:", uf.URI, "=>", pathname)
if err := os.MkdirAll(path.Dir(pathname), 0777); err != nil {
return nil, err
} else if err := getCloudFile(uf.URI, pathname); err != nil {
return nil, err
}
}
return exercice.ImportFile(pathname, uf.URI)
}
func getCloudFile(pathname string, dest string) error {
client := http.Client{}
if req, err := http.NewRequest("GET", CloudDAVBase+pathname, nil); err != nil {
return err
} else {
req.SetBasicAuth(CloudUsername, CloudPassword)
if resp, err := client.Do(req); err != nil {
return err
} else {
defer resp.Body.Close()
if fd, err := os.Create(dest); err != nil {
return err
} else {
defer fd.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
} else {
writer := bufio.NewWriter(fd)
reader := bufio.NewReader(resp.Body)
reader.WriteTo(writer)
writer.Flush()
}
}
}
}
return nil
}

View file

@ -1,210 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"srs.epita.fr/fic-server/libfic"
)
var ApiTeamsRouting = map[string]DispatchFunction{
"GET": listTeam,
"PUT": creationTeamMembers,
"POST": creationTeam,
"DELETE": deletionTeam,
}
func nginxGenTeam() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
ret += fmt.Sprintf(" if ($ssl_client_s_dn ~ \"/C=FR/ST=France/O=Epita/OU=SRS/CN=%s\") { set $team \"%s\"; }\n", team.InitialName, team.InitialName)
}
return ret, nil
}
}
func bindingTeams() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err != nil {
return "", err
} else {
var mbs []string
for _, member := range members {
mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname))
}
ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
}
}
return ret, nil
}
}
type uploadedTeam struct {
Name string
Color uint32
}
type uploadedMember struct {
Firstname string
Lastname string
Nickname string
Company string
}
func listTeam(args []string, body []byte) (interface{}, error) {
if len(args) >= 2 {
var team *fic.Team
if tid, err := strconv.Atoi(args[0]); err != nil {
if t, err := fic.GetTeamByInitialName(args[0]); err != nil {
return nil, err
} else {
team = &t
}
} else {
if tid == 0 {
team = nil
} else if t, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
team = &t
}
}
if args[1] == "my.json" {
return fic.MyJSONTeam(team, true)
} else if args[1] == "wait.json" {
return fic.MyJSONTeam(team, false)
} else if args[1] == "stats.json" {
if team != nil {
return team.GetStats()
} else {
return fic.GetTeamsStats(nil)
}
} else if args[1] == "tries" {
return fic.GetTries(team, nil)
} else if team != nil && args[1] == "members" {
return team.GetMembers()
} else if args[1] == "certificate" && team != nil {
return CertificateAPI(*team, args[2:])
} else if team != nil && args[1] == "name" {
return team.Name, nil
}
} else if len(args) == 1 {
if args[0] == "teams.json" {
return fic.ExportTeams()
} else if args[0] == "tries" {
return fic.GetTries(nil, nil)
} else if args[0] == "nginx" {
return nginxGenTeam()
} else if args[0] == "binding" {
return bindingTeams()
} else if tid, err := strconv.Atoi(string(args[0])); err != nil {
return fic.GetTeamByInitialName(args[0])
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
return team, nil
}
} else if len(args) == 0 {
// List all teams
return fic.GetTeams()
}
return nil, nil
}
func creationTeam(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
// List given team
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
var members []uploadedMember
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
}
for _, member := range members {
team.AddMember(member.Firstname, member.Lastname, member.Nickname, member.Company)
}
return team.GetMembers()
}
} else if len(args) == 0 {
// Create a new team
var ut uploadedTeam
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
return fic.CreateTeam(ut.Name, ut.Color)
} else {
return nil, nil
}
}
func creationTeamMembers(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
// List given team
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
var member uploadedMember
if err := json.Unmarshal(body, &member); err != nil {
return nil, err
}
team.AddMember(member.Firstname, member.Lastname, member.Nickname, member.Company)
return team.GetMembers()
}
} else if len(args) == 0 {
// Create a new team
var members []uploadedMember
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
}
if team, err := fic.CreateTeam("", 0); err != nil {
return nil, err
} else {
for _, member := range members {
if _, err := team.AddMember(member.Firstname, member.Lastname, member.Nickname, member.Company); err != nil {
return nil, err
}
}
return team, nil
}
} else {
return nil, nil
}
}
func deletionTeam(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
return team.Delete()
}
} else {
return nil, nil
}
}

View file

@ -1,164 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"srs.epita.fr/fic-server/libfic"
)
var ApiThemesRouting = map[string]DispatchFunction{
"GET": listTheme,
"PATCH": updateTheme,
"POST": creationTheme,
"DELETE": deletionTheme,
}
func bindingFiles() (string, error) {
if files, err := fic.GetFiles(); err != nil {
return "", err
} else {
ret := ""
for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
}
return ret, nil
}
}
func getTheme(args []string) (fic.Theme, error) {
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return fic.Theme{}, err
} else {
return fic.GetTheme(tid)
}
}
func getExercice(args []string) (fic.Exercice, error) {
if theme, err := getTheme(args); err != nil {
return fic.Exercice{}, err
} else if eid, err := strconv.Atoi(string(args[1])); err != nil {
return fic.Exercice{}, err
} else {
return theme.GetExercice(eid)
}
}
func listTheme(args []string, body []byte) (interface{}, error) {
if len(args) == 3 {
if e, err := getExercice(args); err != nil {
return nil, err
} else {
if args[2] == "files" {
return e.GetFiles()
} else if args[2] == "keys" {
return e.GetKeys()
}
}
} else if len(args) == 2 {
if args[1] == "exercices" {
if theme, err := getTheme(args); err != nil {
return nil, err
} else {
return theme.GetExercices()
}
} else {
return getExercice(args)
}
} else if len(args) == 1 {
if args[0] == "files-bindings" {
return bindingFiles()
} else if args[0] == "themes.json" {
return fic.ExportThemes()
} else {
return getTheme(args)
}
} else if len(args) == 0 {
// List all themes
return fic.GetThemes()
}
return nil, nil
}
type uploadedTheme struct {
Name string
Authors string
}
func creationTheme(args []string, body []byte) (interface{}, error) {
if len(args) >= 1 {
if theme, err := getTheme(args); err != nil {
return nil, err
} else {
return createExercice(theme, args[1:], body)
}
} else if len(args) == 0 {
// Create a new theme
var ut uploadedTheme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
}
return fic.CreateTheme(ut.Name, ut.Authors)
} else {
return nil, nil
}
}
func updateTheme(args []string, body []byte) (interface{}, error) {
if len(args) == 2 {
// Update an exercice
var ue fic.Exercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Exercice's title not filled")
}
if _, err := ue.Update(); err != nil {
return nil, err
}
return ue, nil
} else if len(args) == 1 {
// Update a theme
var ut fic.Theme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
}
return ut.Update()
} else {
return nil, nil
}
}
func deletionTheme(args []string, body []byte) (interface{}, error) {
if len(args) == 2 {
if exercice, err := getExercice(args); err != nil {
return nil, err
} else {
return exercice.Delete()
}
} else if len(args) == 1 {
if theme, err := getTheme(args); err != nil {
return nil, err
} else {
return theme.Delete()
}
} else {
return nil, nil
}
}

View file

@ -1,11 +0,0 @@
package main
import ()
var ApiVersionRouting = map[string]DispatchFunction{
"GET": showVersion,
}
func showVersion(args []string, body []byte) (interface{}, error) {
return map[string]interface{}{"version": 0.1}, nil
}

View file

@ -1,14 +1,14 @@
#!/bin/sh
#!/bin/bash
BASEURL="http://localhost:8081"
BASEURI="https://srs.epita.fr/owncloud/remote.php/webdav/FIC 2016"
BASEFILE="/files"
BASEURI="https://owncloud.srs.epita.fr/remote.php/webdav/FIC 2017"
BASEFILE="/mnt/fic/"
CLOUDPASS=fic:'f>t\nV33R|(+?$i*'
new_theme() {
NAME=`echo $1 | sed 's/"/\\\\"/g'`
AUTHORS=`echo $2 | sed 's/"/\\\\"/g'`
curl -f -s -d "{\"name\": \"$NAME\", \"authors\": \"$AUTHORS\"}" "${BASEURL}/api/themes/" |
curl -f -s -d "{\"name\": \"$NAME\", \"authors\": \"$AUTHORS\"}" "${BASEURL}/api/themes" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
@ -16,66 +16,136 @@ new_exercice() {
THEME="$1"
TITLE=`echo "$2" | sed 's/"/\\\\"/g'`
STATEMENT=`echo "$3" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/<br>/g'`
HINT=`echo "$4" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/<br>/g'`
DEPEND="$5"
GAIN="$6"
VIDEO="$7"
DEPEND="$4"
GAIN="$5"
VIDEO="$6"
curl -f -s -d "{\"title\": \"$TITLE\", \"statement\": \"$STATEMENT\", \"hint\": \"$HINT\", \"depend\": $DEPEND, \"gain\": $GAIN, \"videoURI\": \"$VIDEO\"}" "${BASEURL}/api/themes/$THEME" |
curl -f -s -d "{\"title\": \"$TITLE\", \"statement\": \"$STATEMENT\", \"depend\": $DEPEND, \"gain\": $GAIN, \"videoURI\": \"$VIDEO\"}" "${BASEURL}/api/themes/$THEME/exercices" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_file() {
new_file() (
THEME="$1"
EXERCICE="$2"
URI="$3"
DIGEST="$4"
ARGS="$5"
curl -f -s -d "{\"URI\": \"${BASEFILE}${URI}\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/files" |
FIRST=
PARTS=$(echo "$ARGS" | while read arg
do
[ -n "$arg" ] && {
[ -z "${FIRST}" ] || echo -n ","
echo "\"$arg\""
}
FIRST=1
done)
[ -n "${DIGEST}" ] && DIGEST=", \"digest\": \"${DIGEST}\""
cat <<EOF >&2
{"path": "${BASEFILE}${URI}"${DIGEST}, "parts": [${PARTS}]}
EOF
# curl -f -s -d "{\"URI\": \"${BASEFILE}${URI}\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/files" |
curl -f -s -d @- "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/files" <<EOF | grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
{"path": "${BASEFILE}${URI}"${DIGEST}, "parts": [${PARTS}]}
EOF
)
new_hint() {
THEME="$1"
EXERCICE="$2"
TITLE=`echo "$3" | sed 's/"/\\\\"/g'`
CONTENT=`echo "$4" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/<br>/g'`
COST="$5"
URI="$6"
[ -n "${CONTENT}" ] && CONTENT=", \"content\": \"${CONTENT}\""
[ -n "${URI}" ] && URI=", \"path\": \"${BASEFILE}${URI}\""
curl -f -s -d "{\"title\": \"$TITLE\"$CONTENT$URI, \"cost\": $COST}" "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/hints" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_key() {
THEME="$1"
EXERCICE="$2"
NAME="$3"
KEY=`echo $4 | sed 's/"/\\\\"/g' | sed 's#\\\\#\\\\\\\\#g'`
TYPE="$3"
KEY=`echo $4 | sed 's#\\\\#\\\\\\\\#g' | sed 's/"/\\\\"/g'`
curl -f -s -d "{\"name\": \"$NAME\", \"key\": \"$KEY\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/keys" |
curl -f -s -d "{\"type\": \"$TYPE\", \"key\": \"$KEY\"}" "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/keys" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
get_dir_from_cloud() {
curl -f -s -X PROPFIND -u "${CLOUDPASS}" "${BASEURI}$1" | xmllint --format - | grep 'd:href' | sed -E 's/^.*>(.*)<.*$/\1/'
}
get_dir() {
ls "${BASEFILE}$1" 2> /dev/null
}
#alias get_dir=get_dir_from_cloud
get_file_from_cloud() {
curl -f -s -u "${CLOUDPASS}" "${BASEURI}$1" | tr -d '\r'
}
get_file() {
cat "${BASEFILE}$1" 2> /dev/null | tr -d '\r'
echo
}
#alias get_file=get_file_from_cloud
unhtmlentities() {
cat | sed -E 's/%20/ /g' | sed -E "s/%27/'/g" | sed -E 's/%c3%a9/é/g' | sed -E 's/%c3%a8/è/g'
}
# Theme
curl -f -s -X PROPFIND -u "${CLOUDPASS}" "${BASEURI}" | xmllint --format - | grep 'd:href' | sed -E 's/^.*>(.*)<.*$/\1/' | sed 1d | tac | while read f; do basename "$f"; done | while read THEME_URI
{
if [ $# -ge 1 ]; then
echo $1
else
get_dir ""
fi
} | while read f; do basename "$f"; done | while read THEME_URI
do
THM_BASEURI="/${THEME_URI}/"
THEME_NAME=$(echo "${THEME_URI}" | sed -E 's/%20/ /g' | sed -E 's/%c3%a9/é/g' | sed -E 's/%c3%a8/è/g')
THEME_AUTHORS=$(curl -f -s -u "${CLOUDPASS}" "${BASEURI}${THM_BASEURI}/AUTHORS.txt" | sed 's/$/,/' | xargs)
THEME_NAME=$(echo "${THEME_URI#*-}" | unhtmlentities)
THEME_AUTHORS=$(get_file "${THM_BASEURI}/AUTHORS.txt" | sed '/^$/d;s/$/, /' | tr -d '\n' | sed 's/, $//')
THEME_ID=`new_theme "$THEME_NAME" "$THEME_AUTHORS"`
if [ -z "$THEME_ID" ]; then
echo -e "\e[31;01m!!! An error occured during theme add\e[00m"
continue
continue
else
echo -e "\e[33m>>> New theme created:\e[00m $THEME_ID - $THEME_NAME"
fi
LAST=null
EXO_NUM=0
curl -f -s -X PROPFIND -u "${CLOUDPASS}" "${BASEURI}${THM_BASEURI}" | xmllint --format - | grep 'd:href' | sed -E 's/^.*>(.*)<.*$/\1/' | sed -E 's/%20/ /g' | sed -E 's/%c3%a9/é/g' | sed -E 's/%c3%a8/è/g' | sed 1d | while read f; do basename "$f"; done | while read EXO_NAME
do
if ! echo $EXO_NAME | grep Exercice > /dev/null
then
continue
{
if [ $# -ge 2 ]; then
echo "$2"
else
get_dir "${THM_BASEURI}"
fi
} | while read f; do basename "$f"; done | while read EXO_URI
do
case ${EXO_URI} in
[0-9]-*)
;;
*)
continue;;
esac
EXO_NUM=$((EXO_NUM + 1))
#EXO_NUM=$((EXO_NUM + 1))
EXO_NUM=${EXO_URI%-*}
EXO_NAME=$(echo "${EXO_URI#*-}" | unhtmlentities)
echo
echo -e "\e[36m--- Filling exercice ${EXO_NUM} in theme ${THEME_NAME}\e[00m"
EXO_BASEURI="${EXO_NAME}/"
EXO_BASEURI="${EXO_URI}/"
FILES=$(curl -f -s -X PROPFIND -u "${CLOUDPASS}" "${BASEURI}${THM_BASEURI}${EXO_BASEURI}" | xmllint --format - | grep 'd:href' | sed -E 's/^.*>(.*)<.*$/\1/' | sed 1d | while read f; do basename $f; done)
EXO_VIDEO=$(echo "$FILES" | grep -E "\.(mov|mkv|mp4|avi|flv|ogv|webm)$" | tail -1)
EXO_VIDEO=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/resolution/" | grep -E "\.(mov|mkv|mp4|avi|flv|ogv|webm)$" | while read f; do basename "$f"; done | tail -1)
[ -n "$EXO_VIDEO" ] && EXO_VIDEO="/resolution${THM_BASEURI}${EXO_BASEURI}resolution/${EXO_VIDEO}"
if [ "${LAST}" = "null" ]; then
echo ">>> Assuming this exercice has no dependency"
@ -84,12 +154,12 @@ do
fi
EXO_GAIN=$((3 * (2 ** $EXO_NUM) - 1))
HINT_COST=$(($EXO_GAIN / 4))
echo ">>> Using default gain: ${EXO_GAIN} points"
EXO_DESC=$(curl -f -s -u "${CLOUDPASS}" "${BASEURI}${THM_BASEURI}${EXO_BASEURI}/description.txt")
EXO_HINT=$(curl -f -s -u "${CLOUDPASS}" "${BASEURI}${THM_BASEURI}${EXO_BASEURI}/hint.txt")
EXO_SCENARIO=$(get_file "${THM_BASEURI}${EXO_BASEURI}/scenario.txt")
EXO_ID=`new_exercice "${THEME_ID}" "${EXO_NAME}" "${EXO_DESC}" "${EXO_HINT}" "${LAST}" "${EXO_GAIN}" "${THM_BASEURI}${EXO_BASEURI}${EXO_VIDEO}"`
EXO_ID=`new_exercice "${THEME_ID}" "${EXO_NAME}" "${EXO_SCENARIO}" "${LAST}" "${EXO_GAIN}" "${EXO_VIDEO}"`
if [ -z "$EXO_ID" ]; then
echo -e "\e[31;01m!!! An error occured during exercice add.\e[00m"
continue
@ -99,10 +169,17 @@ do
# Keys
curl -f -s -u "${CLOUDPASS}" "${BASEURI}${THM_BASEURI}${EXO_BASEURI}/keys.txt" | while read KEYLINE
get_file "${THM_BASEURI}${EXO_BASEURI}/flags.txt" | while read KEYLINE
do
KEY_NAME=$(echo "$KEYLINE" | cut -d : -f 1)
KEY_RAW=$(echo "$KEYLINE" | cut -d : -f 2-)
[ -z "${KEYLINE}" ] && continue
KEY_NAME=$(echo "$KEYLINE" | cut -d$'\t' -f 1)
KEY_RAW=$(echo "$KEYLINE" | cut -d$'\t' -f 2-)
if [ -z "${KEY_RAW}" ] || [ "${KEY_NAME}" = "${KEY_RAW}" ]; then
KEY_NAME=$(echo "$KEYLINE" | cut -d : -f 1)
KEY_RAW=$(echo "$KEYLINE" | cut -d : -f 2-)
fi
if [ -z "${KEY_NAME}" ]; then
KEY_NAME="Flag"
@ -117,16 +194,64 @@ do
done
# Files
for f in $FILES; do echo $f; done | grep -vEi "(ressources|readme|description.txt|hint.txt|keys.txt|${EXO_VIDEO})" |
while read FBASE
# Hints
HINTS=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/hints/" | sed -E 's#(.*)#hints/\1#')
[ -z "${HINTS}" ] && HINTS=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/" | grep ^hint.)
[ -z "${HINTS}" ] && HINTS="hint.txt"
HINT_COUNT=1
echo "${HINTS}" | while read HINT
do
echo "Import file ${BASEURI}${THM_BASEURI}${EXO_BASEURI}${FBASE}"
FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}${FBASE}"`
EXO_HINT=$(get_file "${THM_BASEURI}${EXO_BASEURI}/${HINT}")
if [ -n "$EXO_HINT" ]; then
EXO_HINT_TYPE=$(echo "${EXO_HINT}" | file --mime-type -b -)
if echo "${EXO_HINT_TYPE}" | grep text/ && [ $(echo "${EXO_HINT}" | wc -l) -lt 25 ]; then
HINT_ID=`new_hint "${THEME_ID}" "${EXO_ID}" "Astuce #${HINT_COUNT}" "${EXO_HINT}" "${HINT_COST}"`
else
HINT_ID=`new_hint "${THEME_ID}" "${EXO_ID}" "Astuce #${HINT_COUNT}" "" "${HINT_COST}" "${THM_BASEURI}${EXO_BASEURI}/${HINT}"`
fi
if [ -z "$HINT_ID" ]; then
echo -e "\e[31;01m!!! An error occured during hint import!\e[00m (title=Astuce #${HINT_COUNT};content::${EXO_HINT_TYPE};cost=${HINT_COST})"
else
echo -e "\e[32m>>> New hint added:\e[00m $HINT_ID - Astuce #${HINT_COUNT}"
fi
fi
HINT_COUNT=$(($HINT_COUNT + 1))
done
# Files: splited
get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep -v DIGESTS.txt | grep '[0-9][0-9]$' | sed -E 's/\.?([0-9][0-9])$//' | sort | uniq | while read f; do basename "$f"; done | while read FILE_URI
do
DIGEST=$(get_file "${THM_BASEURI}${EXO_BASEURI}files/DIGESTS.txt" | grep "${FILE_URI}\$" | awk '{ print $1; }')
PARTS=
for part in $(get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep "${FILE_URI}" | sort)
do
PARTS="${PARTS}${BASEFILE}${THM_BASEURI}${EXO_BASEURI}files/${part}
"
done
echo -e "\e[35mImport splited file ${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI} from\e[00m `echo ${PARTS} | tr '\n' ' '`"
FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}" "${DIGEST}" "${PARTS}"`
if [ -z "$FILE_ID" ]; then
echo -e "\e[31;01m!!! An error occured during file import! Please check path.\e[00m"
else
echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FBASE"
echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FILE_URI"
fi
done
# Files: entire
get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep -v DIGESTS.txt | grep -v '[0-9][0-9]$' | while read f; do basename "$f"; done | while read FILE_URI
do
DIGEST=$(get_file "${THM_BASEURI}${EXO_BASEURI}files/DIGESTS.txt" | grep "${FILE_URI}\$" | awk '{ print $1; }')
echo "Import file ${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}"
FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}" "${DIGEST}"`
if [ -z "$FILE_ID" ]; then
echo -e "\e[31;01m!!! An error occured during file import! Please check path.\e[00m"
else
echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FILE_URI"
fi
done

View file

@ -1,7 +1,48 @@
#!/bin/sh
#!/bin/bash
BASEURL="http://localhost:8081"
PART_FILE="Challenge_Liste des participants.csv"
BASEURL="http://127.0.0.1:8081/admin"
GEN_CERTS=0
EXTRA_TEAMS=0
CSV_SPLITER=","
CSV_COL_LASTNAME=1
CSV_COL_FIRSTNAME=2
CSV_COL_NICKNAME=3
CSV_COL_COMPANY=7
CSV_COL_TEAM=7
usage() {
echo "$0 [options] csv_file"
echo " -B -baseurl BASEURL URL to administration endpoint (default: $BASEURL)"
echo " -S -csv-spliter SEP CSV separator (default: $CSV_SPLITER)"
echo " -e -extra-teams NBS Number of extra teams to generate (default: ${EXTRA_TEAMS})"
echo " -c -generate-certificate Should team certificates be generated? (default: no)"
}
# Parse options
while [ "${1:0:1}" = "-" ]
do
case "$1" in
-B|-baseurl)
BASEURL=$2
shift;;
-S|-csv-spliter)
CSV_SPLITER=$2
shift;;
-e|-extra-teams)
EXTRA_TEAMS=$2
shift;;
-c|-generate-certificates)
GEN_CERTS=1;;
*)
echo "Unknown option '$1'"
usage
exit 1;;
esac
shift
done
[ "$#" -lt 1 ] && { usage; exit 1; }
PART_FILE="$1"
new_team() {
head -n "$1" team-names.txt | tail -1 | sed -E 's/^.*\|\[\[([^|]+\|)?([^|]+)\]\][^|]*\|([A-Fa-f0-9]{1,2})\|([A-Fa-f0-9]{1,2})\|([A-Fa-f0-9]{1,2})\|([0-9]{1,3})\|([0-9]{1,3})\|([0-9]{1,3})\|.*$/\6 \7 \8 \2/' |
@ -10,7 +51,11 @@ new_team() {
R=`echo $line | cut -d " " -f 1`
G=`echo $line | cut -d " " -f 2`
B=`echo $line | cut -d " " -f 3`
N=`echo $line | cut -d " " -f 4`
if [ -z "$2" ]; then
N=`echo $line | cut -d " " -f 4`
else
N=`echo -n $2 | tr -d '\r\n'`
fi
COLOR=$((($R*256 + $G) * 256 + $B))
@ -20,7 +65,7 @@ new_team() {
TNUM=0
for i in `seq 12`
for i in $(seq $EXTRA_TEAMS)
do
TNUM=$(($TNUM + 1))
@ -28,31 +73,32 @@ do
TID=`new_team $TNUM`
if ! curl -s -f "${BASEURL}/api/teams/${TID}/certificate" > /dev/null
if [ "${GEN_CERTS}" -eq 1 ] && ! curl -s -f "${BASEURL}/api/teams/${TID}/certificate" > /dev/null
then
curl -s -f "${BASEURL}/api/teams/${TID}/certificate/generate"
fi
echo
done
TMAX=`sed "1d" "$PART_FILE" | cut -d \; -f 15 | sort | uniq | wc -l`
TMAX=`cat "$PART_FILE" | cut -d "${CSV_SPLITER}" -f $CSV_COL_TEAM | sort | uniq | wc -l`
TMAX=$(($TMAX + $TNUM))
sed "1d" "$PART_FILE" | cut -d \; -f 15 | sort | uniq | while read TEAMID
cat "$PART_FILE" | cut -d "${CSV_SPLITER}" -f $CSV_COL_TEAM | sort | uniq | while read TEAMID
do
TNUM=$(($TNUM + 1))
echo "Doing team $TNUM/$TMAX ("$(($TNUM*100/$TMAX))"%)..."
TID=`new_team $TNUM`
TID=`new_team "${TNUM}" "${TEAMID}"`
(
if ! (
echo -n "["
HAS_MEMBER=1
grep ";$TEAMID\$" "$PART_FILE" | while read MEMBER
grep "${CSV_SPLITER}${TEAMID}\$" "$PART_FILE" | while read MEMBER
do
LASTNAME=`echo $MEMBER | cut -d ";" -f 2`
FIRSTNAME=`echo $MEMBER | cut -d ";" -f 3`
COMPANY=`echo $MEMBER | cut -d ";" -f 4`
LASTNAME=`echo $MEMBER | cut -d "${CSV_SPLITER}" -f $CSV_COL_LASTNAME | tr -d "\r\n"`
FIRSTNAME=`echo $MEMBER | cut -d "${CSV_SPLITER}" -f $CSV_COL_FIRSTNAME | tr -d "\r\n"`
NICKNAME=`echo $MEMBER | cut -d "${CSV_SPLITER}" -f $CSV_COL_NICKNAME | tr -d "\r\n"`
COMPANY=`echo $MEMBER | cut -d "${CSV_SPLITER}" -f $CSV_COL_COMPANY | tr -d "\r\n"`
if [ $HAS_MEMBER = 0 ]
then
@ -65,15 +111,16 @@ do
{
"firstname": "$FIRSTNAME",
"lastname": "$LASTNAME",
"nickname": "",
"nickname": "$NICKNAME",
"company": "$COMPANY"
}
EOF
done
echo "]"
) | curl -s -d @- "${BASEURL}/api/teams/${TID}" > /dev/null
if ! curl -s -f "${BASEURL}/api/teams/${TID}/certificate" > /dev/null
) | curl -f -s -d @- "${BASEURL}/api/teams/${TID}"
then
echo "An error occured"
elif [ "${GEN_CERTS}" -eq 1 ] && ! curl -s -f "${BASEURL}/api/teams/${TID}/certificate" > /dev/null
then
curl -s -f "${BASEURL}/api/teams/${TID}/certificate/generate"
fi

62
admin/index.go Normal file
View file

@ -0,0 +1,62 @@
package main
const indextpl = `<!DOCTYPE html>
<html ng-app="FICApp">
<head>
<meta charset="utf-8">
<title>Challenge Forensic - Administration</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<base href="{{.urlbase}}">
<script src="/js/d3.v3.min.js"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="{{.urlbase}}">
<img alt="FIC" src="{{.urlbase}}img/fic.png" style="height: 100%">
</a>
</div>
<ul class="nav navbar-nav">
<li><a href="{{.urlbase}}teams">&Eacute;quipes</a></li>
<li><a href="{{.urlbase}}themes">Thèmes</a></li>
<li><a href="{{.urlbase}}exercices">Exercices</a></li>
<li><a href="{{.urlbase}}public">Public</a></li>
<li><a href="{{.urlbase}}events">&Eacute;vénements</a></li>
<li><a href="{{.urlbase}}settings">Paramètres</a></li>
</ul>
<p id="clock" class="navbar-text navbar-right" ng-controller="CountdownController">
<span ng-show="startIn > 0">
Démarrage dans :
<span>{{"{{ startIn }}"}}</span>"
<span class="point">|</span>
</span>
<span id="hours">{{"{{ time.hours | time }}"}}</span>
<span class="point">:</span>
<span id="min">{{"{{ time.minutes | time }}"}}</span>
<span class="point">:</span>
<span id="sec">{{"{{ time.seconds | time }}"}}</span>
</p>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-sm-12" ng-view></div>
</div>
</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/angular.min.js"></script>
<script src="{{.urlbase}}js/angular-resource.min.js"></script>
<script src="/js/angular-route.min.js"></script>
<script src="/js/angular-sanitize.min.js"></script>
<script src="{{.urlbase}}js/app.js"></script>
</body>
</html>
`

View file

@ -6,37 +6,40 @@ import (
"log"
"net/http"
"os"
"path"
"path/filepath"
"text/template"
"srs.epita.fr/fic-server/admin/api"
"srs.epita.fr/fic-server/libfic"
)
var PKIDir string
var SubmissionDir string
var BaseURL string
var CloudDAVBase string
var CloudUsername string
var CloudPassword string
var StaticDir string
func main() {
var bind = flag.String("bind", "0.0.0.0:8081", "Bind port/socket")
var bind = flag.String("bind", "127.0.0.1:8081", "Bind port/socket")
var dsn = flag.String("dsn", "fic:fic@/fic", "DSN to connect to the MySQL server")
flag.StringVar(&BaseURL, "baseurl", "http://fic.srs.epita.fr/", "URL prepended to each URL")
var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
flag.StringVar(&SubmissionDir, "submission", "./submissions/", "Base directory where save submissions")
flag.StringVar(&PKIDir, "pki", "./pki/", "Base directory where found PKI scripts")
flag.StringVar(&StaticDir, "static", "./htdocs-admin/", "Directory containing static files")
flag.StringVar(&api.TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
flag.StringVar(&fic.FilesDir, "files", "./FILES/", "Base directory where found challenges files, local part")
flag.StringVar(&CloudDAVBase, "clouddav", "https://srs.epita.fr/owncloud/remote.php/webdav/FIC 2016",
flag.StringVar(&api.CloudDAVBase, "clouddav", "https://srs.epita.fr/owncloud/remote.php/webdav/FIC 2016",
"Base directory where found challenges files, cloud part")
flag.StringVar(&CloudUsername, "clouduser", "fic", "Username used to sync")
flag.StringVar(&CloudPassword, "cloudpass", "", "Password used to sync")
flag.StringVar(&api.CloudUsername, "clouduser", "fic", "Username used to sync")
flag.StringVar(&api.CloudPassword, "cloudpass", "", "Password used to sync")
flag.BoolVar(&api.RapidImport, "rapidimport", false, "Don't try to reimport an existing file")
flag.BoolVar(&fic.OptionalDigest, "optionaldigest", false, "Is the digest required when importing files?")
flag.Parse()
log.SetPrefix("[admin] ")
var staticDir string
var err error
log.Println("Checking paths...")
if staticDir, err = filepath.Abs("./static/"); err != nil {
if StaticDir, err = filepath.Abs(StaticDir); err != nil {
log.Fatal(err)
}
if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
@ -48,9 +51,23 @@ func main() {
if SubmissionDir, err = filepath.Abs(SubmissionDir); err != nil {
log.Fatal(err)
}
if api.TeamsDir, err = filepath.Abs(api.TeamsDir); err != nil {
log.Fatal(err)
}
if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
log.Fatal(err)
}
if *baseURL != "/" {
tmp := path.Clean(*baseURL)
baseURL = &tmp
} else {
tmp := ""
baseURL = &tmp
}
if err := os.Chdir(PKIDir); err != nil {
log.Fatal("Unable to enter PKI directory at: ", err)
}
log.Println("Opening database...")
if err := fic.DBInit(fmt.Sprintf("%s?parseTime=true", *dsn)); err != nil {
@ -63,17 +80,18 @@ func main() {
log.Fatal("Cannot create database: ", err)
}
os.Chdir(PKIDir)
log.Println("Changing base URL to", *baseURL,"...")
if file, err := os.OpenFile(path.Join(StaticDir, "index.html"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0644)); err != nil {
log.Println("Unable to open index.html: ", err)
} else if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
log.Println("Cannot create template: ", err)
} else if err := indexTmpl.Execute(file, map[string]string{"urlbase": path.Clean(path.Join(*baseURL + "/", "nuke"))[:len(path.Clean(path.Join(*baseURL + "/", "nuke"))) - 4]}); err != nil {
log.Println("An error occurs during template execution: ", err)
}
log.Println("Registering handlers...")
mux := http.NewServeMux()
mux.Handle("/api/", http.StripPrefix("/api", ApiHandler()))
mux.Handle("/teams/", StaticHandler(staticDir))
mux.Handle("/themes/", StaticHandler(staticDir))
mux.Handle("/", http.FileServer(http.Dir(staticDir)))
log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
if err := http.ListenAndServe(*bind, mux); err != nil {
if err := http.ListenAndServe(*bind, http.StripPrefix(*baseURL, api.Router())); err != nil {
log.Fatal("Unable to listen and serve: ", err)
}
}

View file

@ -3,16 +3,48 @@ package main
import (
"net/http"
"path"
"srs.epita.fr/fic-server/admin/api"
"github.com/julienschmidt/httprouter"
)
type staticRouting struct {
StaticDir string
}
func init() {
api.Router().GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
})
api.Router().GET("/exercices/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
})
api.Router().GET("/events/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
})
api.Router().GET("/public", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
})
api.Router().GET("/settings/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
})
api.Router().GET("/teams/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
})
api.Router().GET("/themes/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
})
func StaticHandler(staticDir string) http.Handler {
return staticRouting{staticDir}
}
func (a staticRouting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path.Join(a.StaticDir, "index.html"))
api.Router().GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path))
})
api.Router().GET("/fonts/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path))
})
api.Router().GET("/img/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path))
})
api.Router().GET("/js/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path))
})
api.Router().GET("/views/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
http.ServeFile(w, r, path.Join(StaticDir, r.URL.Path))
})
}

View file

@ -5,7 +5,7 @@
<title>Challenge Forensic - Administration</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<base href="/">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="/js/d3.v3.min.js"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
@ -13,7 +13,7 @@
<div class="navbar-header">
<a class="navbar-brand" href="/">
<img alt="FIC" src="img/fic.png" style="height: 100%">
<img alt="FIC" src="/img/fic.png" style="height: 100%">
</a>
</div>
@ -21,10 +21,17 @@
<li><a href="/teams">&Eacute;quipes</a></li>
<li><a href="/themes">Thèmes</a></li>
<li><a href="/exercices">Exercices</a></li>
<li><a href="/public">Public</a></li>
<li><a href="/events">&Eacute;vénements</a></li>
<li><a href="/settings">Paramètres</a></li>
</ul>
<p id="clock" class="navbar-text navbar-right" ng-controller="CountdownController">
<span ng-show="startIn > 0">
Démarrage dans :
<span>{{ startIn }}</span>"
<span class="point">|</span>
</span>
<span id="hours">{{ time.hours | time }}</span>
<span class="point">:</span>
<span id="min">{{ time.minutes | time }}</span>
@ -46,6 +53,7 @@
<script src="/js/angular.min.js"></script>
<script src="/js/angular-resource.min.js"></script>
<script src="/js/angular-route.min.js"></script>
<script src="/js/angular-sanitize.min.js"></script>
<script src="/js/app.js"></script>
</body>
</html>

View file

@ -1,15 +0,0 @@
/*
AngularJS v1.4.8
(c) 2010-2015 Google, Inc. http://angularjs.org
License: MIT
*/
(function(p,c,C){'use strict';function v(r,h,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,f,b,d,y){function z(){k&&(g.cancel(k),k=null);l&&(l.$destroy(),l=null);m&&(k=g.leave(m),k.then(function(){k=null}),m=null)}function x(){var b=r.current&&r.current.locals;if(c.isDefined(b&&b.$template)){var b=a.$new(),d=r.current;m=y(b,function(b){g.enter(b,null,m||f).then(function(){!c.isDefined(t)||t&&!a.$eval(t)||h()});z()});l=d.scope=b;l.$emit("$viewContentLoaded");
l.$eval(w)}else z()}var l,m,k,t=b.autoscroll,w=b.onload||"";a.$on("$routeChangeSuccess",x);x()}}}function A(c,h,g){return{restrict:"ECA",priority:-400,link:function(a,f){var b=g.current,d=b.locals;f.html(d.$template);var y=c(f.contents());b.controller&&(d.$scope=a,d=h(b.controller,d),b.controllerAs&&(a[b.controllerAs]=d),f.data("$ngControllerController",d),f.children().data("$ngControllerController",d));y(a)}}}p=c.module("ngRoute",["ng"]).provider("$route",function(){function r(a,f){return c.extend(Object.create(a),
f)}function h(a,c){var b=c.caseInsensitiveMatch,d={originalPath:a,regexp:a},g=d.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,c,b,d){a="?"===d?d:null;d="*"===d?d:null;g.push({name:b,optional:!!a});c=c||"";return""+(a?"":c)+"(?:"+(a?c:"")+(d&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");d.regexp=new RegExp("^"+a+"$",b?"i":"");return d}var g={};this.when=function(a,f){var b=c.copy(f);c.isUndefined(b.reloadOnSearch)&&(b.reloadOnSearch=!0);
c.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);g[a]=c.extend(b,a&&h(a,b));if(a){var d="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";g[d]=c.extend({redirectTo:a},h(d,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,f,b,d,h,p,x){function l(b){var e=s.current;
(v=(n=k())&&e&&n.$$route===e.$$route&&c.equals(n.pathParams,e.pathParams)&&!n.reloadOnSearch&&!w)||!e&&!n||a.$broadcast("$routeChangeStart",n,e).defaultPrevented&&b&&b.preventDefault()}function m(){var u=s.current,e=n;if(v)u.params=e.params,c.copy(u.params,b),a.$broadcast("$routeUpdate",u);else if(e||u)w=!1,(s.current=e)&&e.redirectTo&&(c.isString(e.redirectTo)?f.path(t(e.redirectTo,e.params)).search(e.params).replace():f.url(e.redirectTo(e.pathParams,f.path(),f.search())).replace()),d.when(e).then(function(){if(e){var a=
c.extend({},e.resolve),b,f;c.forEach(a,function(b,e){a[e]=c.isString(b)?h.get(b):h.invoke(b,null,null,e)});c.isDefined(b=e.template)?c.isFunction(b)&&(b=b(e.params)):c.isDefined(f=e.templateUrl)&&(c.isFunction(f)&&(f=f(e.params)),c.isDefined(f)&&(e.loadedTemplateUrl=x.valueOf(f),b=p(f)));c.isDefined(b)&&(a.$template=b);return d.all(a)}}).then(function(f){e==s.current&&(e&&(e.locals=f,c.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,u))},function(b){e==s.current&&a.$broadcast("$routeChangeError",
e,u,b)})}function k(){var a,b;c.forEach(g,function(d,g){var q;if(q=!b){var h=f.path();q=d.keys;var l={};if(d.regexp)if(h=d.regexp.exec(h)){for(var k=1,m=h.length;k<m;++k){var n=q[k-1],p=h[k];n&&p&&(l[n.name]=p)}q=l}else q=null;else q=null;q=a=q}q&&(b=r(d,{params:c.extend({},f.search(),a),pathParams:a}),b.$$route=d)});return b||g[null]&&r(g[null],{params:{},pathParams:{}})}function t(a,b){var d=[];c.forEach((a||"").split(":"),function(a,c){if(0===c)d.push(a);else{var f=a.match(/(\w+)(?:[?*])?(.*)/),
g=f[1];d.push(b[g]);d.push(f[2]||"");delete b[g]}});return d.join("")}var w=!1,n,v,s={routes:g,reload:function(){w=!0;a.$evalAsync(function(){l();m()})},updateParams:function(a){if(this.current&&this.current.$$route)a=c.extend({},this.current.params,a),f.path(t(this.current.$$route.originalPath,a)),f.search(a);else throw B("norout");}};a.$on("$locationChangeStart",l);a.$on("$locationChangeSuccess",m);return s}]});var B=c.$$minErr("ngRoute");p.provider("$routeParams",function(){this.$get=function(){return{}}});
p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScroll","$animate"];A.$inject=["$compile","$controller","$route"]})(window,window.angular);
//# sourceMappingURL=angular-route.min.js.map

1
admin/static/js/angular-route.min.js vendored Symbolic link
View file

@ -0,0 +1 @@
../../../frontend/static/js/angular-route.min.js

1
admin/static/js/angular-sanitize.min.js vendored Symbolic link
View file

@ -0,0 +1 @@
../../../frontend/static/js/angular-sanitize.min.js

View file

@ -1,295 +0,0 @@
/*
AngularJS v1.4.8
(c) 2010-2015 Google, Inc. http://angularjs.org
License: MIT
*/
(function(S,X,u){'use strict';function G(a){return function(){var b=arguments[0],d;d="["+(a?a+":":"")+b+"] http://errors.angularjs.org/1.4.8/"+(a?a+"/":"")+b;for(b=1;b<arguments.length;b++){d=d+(1==b?"?":"&")+"p"+(b-1)+"=";var c=encodeURIComponent,e;e=arguments[b];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;d+=c(e)}return Error(d)}}function za(a){if(null==a||Xa(a))return!1;if(I(a)||E(a)||B&&a instanceof B)return!0;
var b="length"in Object(a)&&a.length;return Q(b)&&(0<=b&&b-1 in a||"function"==typeof a.item)}function n(a,b,d){var c,e;if(a)if(z(a))for(c in a)"prototype"==c||"length"==c||"name"==c||a.hasOwnProperty&&!a.hasOwnProperty(c)||b.call(d,a[c],c,a);else if(I(a)||za(a)){var f="object"!==typeof a;c=0;for(e=a.length;c<e;c++)(f||c in a)&&b.call(d,a[c],c,a)}else if(a.forEach&&a.forEach!==n)a.forEach(b,d,a);else if(nc(a))for(c in a)b.call(d,a[c],c,a);else if("function"===typeof a.hasOwnProperty)for(c in a)a.hasOwnProperty(c)&&
b.call(d,a[c],c,a);else for(c in a)qa.call(a,c)&&b.call(d,a[c],c,a);return a}function oc(a,b,d){for(var c=Object.keys(a).sort(),e=0;e<c.length;e++)b.call(d,a[c[e]],c[e]);return c}function pc(a){return function(b,d){a(d,b)}}function Td(){return++nb}function Mb(a,b,d){for(var c=a.$$hashKey,e=0,f=b.length;e<f;++e){var g=b[e];if(H(g)||z(g))for(var h=Object.keys(g),k=0,l=h.length;k<l;k++){var m=h[k],r=g[m];d&&H(r)?da(r)?a[m]=new Date(r.valueOf()):Ma(r)?a[m]=new RegExp(r):r.nodeName?a[m]=r.cloneNode(!0):
Nb(r)?a[m]=r.clone():(H(a[m])||(a[m]=I(r)?[]:{}),Mb(a[m],[r],!0)):a[m]=r}}c?a.$$hashKey=c:delete a.$$hashKey;return a}function M(a){return Mb(a,ra.call(arguments,1),!1)}function Ud(a){return Mb(a,ra.call(arguments,1),!0)}function ea(a){return parseInt(a,10)}function Ob(a,b){return M(Object.create(a),b)}function x(){}function Ya(a){return a}function na(a){return function(){return a}}function qc(a){return z(a.toString)&&a.toString!==sa}function q(a){return"undefined"===typeof a}function y(a){return"undefined"!==
typeof a}function H(a){return null!==a&&"object"===typeof a}function nc(a){return null!==a&&"object"===typeof a&&!rc(a)}function E(a){return"string"===typeof a}function Q(a){return"number"===typeof a}function da(a){return"[object Date]"===sa.call(a)}function z(a){return"function"===typeof a}function Ma(a){return"[object RegExp]"===sa.call(a)}function Xa(a){return a&&a.window===a}function Za(a){return a&&a.$evalAsync&&a.$watch}function $a(a){return"boolean"===typeof a}function sc(a){return a&&Q(a.length)&&
Vd.test(sa.call(a))}function Nb(a){return!(!a||!(a.nodeName||a.prop&&a.attr&&a.find))}function Wd(a){var b={};a=a.split(",");var d;for(d=0;d<a.length;d++)b[a[d]]=!0;return b}function ta(a){return F(a.nodeName||a[0]&&a[0].nodeName)}function ab(a,b){var d=a.indexOf(b);0<=d&&a.splice(d,1);return d}function bb(a,b){function d(a,b){var d=b.$$hashKey,e;if(I(a)){e=0;for(var f=a.length;e<f;e++)b.push(c(a[e]))}else if(nc(a))for(e in a)b[e]=c(a[e]);else if(a&&"function"===typeof a.hasOwnProperty)for(e in a)a.hasOwnProperty(e)&&
(b[e]=c(a[e]));else for(e in a)qa.call(a,e)&&(b[e]=c(a[e]));d?b.$$hashKey=d:delete b.$$hashKey;return b}function c(a){if(!H(a))return a;var b=e.indexOf(a);if(-1!==b)return f[b];if(Xa(a)||Za(a))throw Aa("cpws");var b=!1,c;I(a)?(c=[],b=!0):sc(a)?c=new a.constructor(a):da(a)?c=new Date(a.getTime()):Ma(a)?(c=new RegExp(a.source,a.toString().match(/[^\/]*$/)[0]),c.lastIndex=a.lastIndex):z(a.cloneNode)?c=a.cloneNode(!0):(c=Object.create(rc(a)),b=!0);e.push(a);f.push(c);return b?d(a,c):c}var e=[],f=[];if(b){if(sc(b))throw Aa("cpta");
if(a===b)throw Aa("cpi");I(b)?b.length=0:n(b,function(a,c){"$$hashKey"!==c&&delete b[c]});e.push(a);f.push(b);return d(a,b)}return c(a)}function ia(a,b){if(I(a)){b=b||[];for(var d=0,c=a.length;d<c;d++)b[d]=a[d]}else if(H(a))for(d in b=b||{},a)if("$"!==d.charAt(0)||"$"!==d.charAt(1))b[d]=a[d];return b||a}function ma(a,b){if(a===b)return!0;if(null===a||null===b)return!1;if(a!==a&&b!==b)return!0;var d=typeof a,c;if(d==typeof b&&"object"==d)if(I(a)){if(!I(b))return!1;if((d=a.length)==b.length){for(c=
0;c<d;c++)if(!ma(a[c],b[c]))return!1;return!0}}else{if(da(a))return da(b)?ma(a.getTime(),b.getTime()):!1;if(Ma(a))return Ma(b)?a.toString()==b.toString():!1;if(Za(a)||Za(b)||Xa(a)||Xa(b)||I(b)||da(b)||Ma(b))return!1;d=$();for(c in a)if("$"!==c.charAt(0)&&!z(a[c])){if(!ma(a[c],b[c]))return!1;d[c]=!0}for(c in b)if(!(c in d)&&"$"!==c.charAt(0)&&y(b[c])&&!z(b[c]))return!1;return!0}return!1}function cb(a,b,d){return a.concat(ra.call(b,d))}function tc(a,b){var d=2<arguments.length?ra.call(arguments,2):
[];return!z(b)||b instanceof RegExp?b:d.length?function(){return arguments.length?b.apply(a,cb(d,arguments,0)):b.apply(a,d)}:function(){return arguments.length?b.apply(a,arguments):b.call(a)}}function Xd(a,b){var d=b;"string"===typeof a&&"$"===a.charAt(0)&&"$"===a.charAt(1)?d=u:Xa(b)?d="$WINDOW":b&&X===b?d="$DOCUMENT":Za(b)&&(d="$SCOPE");return d}function db(a,b){if("undefined"===typeof a)return u;Q(b)||(b=b?2:null);return JSON.stringify(a,Xd,b)}function uc(a){return E(a)?JSON.parse(a):a}function vc(a,
b){var d=Date.parse("Jan 01, 1970 00:00:00 "+a)/6E4;return isNaN(d)?b:d}function Pb(a,b,d){d=d?-1:1;var c=vc(b,a.getTimezoneOffset());b=a;a=d*(c-a.getTimezoneOffset());b=new Date(b.getTime());b.setMinutes(b.getMinutes()+a);return b}function ua(a){a=B(a).clone();try{a.empty()}catch(b){}var d=B("<div>").append(a).html();try{return a[0].nodeType===Na?F(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+F(b)})}catch(c){return F(d)}}function wc(a){try{return decodeURIComponent(a)}catch(b){}}
function xc(a){var b={};n((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=wc(e),y(e)&&(f=y(f)?wc(f):!0,qa.call(b,e)?I(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Qb(a){var b=[];n(a,function(a,c){I(a)?n(a,function(a){b.push(ja(c,!0)+(!0===a?"":"="+ja(a,!0)))}):b.push(ja(c,!0)+(!0===a?"":"="+ja(a,!0)))});return b.length?b.join("&"):""}function ob(a){return ja(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,
"=").replace(/%2B/gi,"+")}function ja(a,b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function Yd(a,b){var d,c,e=Oa.length;for(c=0;c<e;++c)if(d=Oa[c]+b,E(d=a.getAttribute(d)))return d;return null}function Zd(a,b){var d,c,e={};n(Oa,function(b){b+="app";!d&&a.hasAttribute&&a.hasAttribute(b)&&(d=a,c=a.getAttribute(b))});n(Oa,function(b){b+="app";var e;!d&&(e=a.querySelector("["+b.replace(":",
"\\:")+"]"))&&(d=e,c=e.getAttribute(b))});d&&(e.strictDi=null!==Yd(d,"strict-di"),b(d,c?[c]:[],e))}function yc(a,b,d){H(d)||(d={});d=M({strictDi:!1},d);var c=function(){a=B(a);if(a.injector()){var c=a[0]===X?"document":ua(a);throw Aa("btstrpd",c.replace(/</,"&lt;").replace(/>/,"&gt;"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=eb(b,d.strictDi);c.invoke(["$rootScope",
"$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;S&&e.test(S.name)&&(d.debugInfoEnabled=!0,S.name=S.name.replace(e,""));if(S&&!f.test(S.name))return c();S.name=S.name.replace(f,"");fa.resumeBootstrap=function(a){n(a,function(a){b.push(a)});return c()};z(fa.resumeDeferredBootstrap)&&fa.resumeDeferredBootstrap()}function $d(){S.name="NG_ENABLE_DEBUG_INFO!"+S.name;S.location.reload()}
function ae(a){a=fa.element(a).injector();if(!a)throw Aa("test");return a.get("$$testability")}function zc(a,b){b=b||"_";return a.replace(be,function(a,c){return(c?b:"")+a.toLowerCase()})}function ce(){var a;if(!Ac){var b=pb();(oa=q(b)?S.jQuery:b?S[b]:u)&&oa.fn.on?(B=oa,M(oa.fn,{scope:Pa.scope,isolateScope:Pa.isolateScope,controller:Pa.controller,injector:Pa.injector,inheritedData:Pa.inheritedData}),a=oa.cleanData,oa.cleanData=function(b){var c;if(Rb)Rb=!1;else for(var e=0,f;null!=(f=b[e]);e++)(c=
oa._data(f,"events"))&&c.$destroy&&oa(f).triggerHandler("$destroy");a(b)}):B=N;fa.element=B;Ac=!0}}function qb(a,b,d){if(!a)throw Aa("areq",b||"?",d||"required");return a}function Qa(a,b,d){d&&I(a)&&(a=a[a.length-1]);qb(z(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ra(a,b){if("hasOwnProperty"===a)throw Aa("badname",b);}function Bc(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g<f;g++)c=b[g],a&&(a=(e=a)[c]);return!d&&
z(a)?tc(e,a):a}function rb(a){for(var b=a[0],d=a[a.length-1],c,e=1;b!==d&&(b=b.nextSibling);e++)if(c||a[e]!==b)c||(c=B(ra.call(a,0,e))),c.push(b);return c||a}function $(){return Object.create(null)}function de(a){function b(a,b,c){return a[b]||(a[b]=c())}var d=G("$injector"),c=G("ng");a=b(a,"angular",Object);a.$$minErr=a.$$minErr||G;return b(a,"module",function(){var a={};return function(f,g,h){if("hasOwnProperty"===f)throw c("badname","module");g&&a.hasOwnProperty(f)&&(a[f]=null);return b(a,f,function(){function a(b,
d,e,f){f||(f=c);return function(){f[e||"push"]([b,d,arguments]);return v}}function b(a,d){return function(b,e){e&&z(e)&&(e.$$moduleName=f);c.push([a,d,arguments]);return v}}if(!g)throw d("nomod",f);var c=[],e=[],t=[],A=a("$injector","invoke","push",e),v={_invokeQueue:c,_configBlocks:e,_runBlocks:t,requires:g,name:f,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decorator:b("$provide",
"decorator"),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider","directive"),config:A,run:function(a){t.push(a);return this}};h&&A(h);return v})}})}function ee(a){M(a,{bootstrap:yc,copy:bb,extend:M,merge:Ud,equals:ma,element:B,forEach:n,injector:eb,noop:x,bind:tc,toJson:db,fromJson:uc,identity:Ya,isUndefined:q,isDefined:y,isString:E,isFunction:z,isObject:H,isNumber:Q,isElement:Nb,isArray:I,
version:fe,isDate:da,lowercase:F,uppercase:sb,callbacks:{counter:0},getTestability:ae,$$minErr:G,$$csp:Ba,reloadWithDebugInfo:$d});Sb=de(S);Sb("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:ge});a.provider("$compile",Cc).directive({a:he,input:Dc,textarea:Dc,form:ie,script:je,select:ke,style:le,option:me,ngBind:ne,ngBindHtml:oe,ngBindTemplate:pe,ngClass:qe,ngClassEven:re,ngClassOdd:se,ngCloak:te,ngController:ue,ngForm:ve,ngHide:we,ngIf:xe,ngInclude:ye,ngInit:ze,ngNonBindable:Ae,
ngPluralize:Be,ngRepeat:Ce,ngShow:De,ngStyle:Ee,ngSwitch:Fe,ngSwitchWhen:Ge,ngSwitchDefault:He,ngOptions:Ie,ngTransclude:Je,ngModel:Ke,ngList:Le,ngChange:Me,pattern:Ec,ngPattern:Ec,required:Fc,ngRequired:Fc,minlength:Gc,ngMinlength:Gc,maxlength:Hc,ngMaxlength:Hc,ngValue:Ne,ngModelOptions:Oe}).directive({ngInclude:Pe}).directive(tb).directive(Ic);a.provider({$anchorScroll:Qe,$animate:Re,$animateCss:Se,$$animateQueue:Te,$$AnimateRunner:Ue,$browser:Ve,$cacheFactory:We,$controller:Xe,$document:Ye,$exceptionHandler:Ze,
$filter:Jc,$$forceReflow:$e,$interpolate:af,$interval:bf,$http:cf,$httpParamSerializer:df,$httpParamSerializerJQLike:ef,$httpBackend:ff,$xhrFactory:gf,$location:hf,$log:jf,$parse:kf,$rootScope:lf,$q:mf,$$q:nf,$sce:of,$sceDelegate:pf,$sniffer:qf,$templateCache:rf,$templateRequest:sf,$$testability:tf,$timeout:uf,$window:vf,$$rAF:wf,$$jqLite:xf,$$HashMap:yf,$$cookieReader:zf})}])}function fb(a){return a.replace(Af,function(a,d,c,e){return e?c.toUpperCase():c}).replace(Bf,"Moz$1")}function Kc(a){a=a.nodeType;
return 1===a||!a||9===a}function Lc(a,b){var d,c,e=b.createDocumentFragment(),f=[];if(Tb.test(a)){d=d||e.appendChild(b.createElement("div"));c=(Cf.exec(a)||["",""])[1].toLowerCase();c=ka[c]||ka._default;d.innerHTML=c[1]+a.replace(Df,"<$1></$2>")+c[2];for(c=c[0];c--;)d=d.lastChild;f=cb(f,d.childNodes);d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";n(f,function(a){e.appendChild(a)});return e}function N(a){if(a instanceof N)return a;var b;E(a)&&(a=U(a),
b=!0);if(!(this instanceof N)){if(b&&"<"!=a.charAt(0))throw Ub("nosel");return new N(a)}if(b){b=X;var d;a=(d=Ef.exec(a))?[b.createElement(d[1])]:(d=Lc(a,b))?d.childNodes:[]}Mc(this,a)}function Vb(a){return a.cloneNode(!0)}function ub(a,b){b||vb(a);if(a.querySelectorAll)for(var d=a.querySelectorAll("*"),c=0,e=d.length;c<e;c++)vb(d[c])}function Nc(a,b,d,c){if(y(c))throw Ub("offargs");var e=(c=wb(a))&&c.events,f=c&&c.handle;if(f)if(b){var g=function(b){var c=e[b];y(d)&&ab(c||[],d);y(d)&&c&&0<c.length||
(a.removeEventListener(b,f,!1),delete e[b])};n(b.split(" "),function(a){g(a);xb[a]&&g(xb[a])})}else for(b in e)"$destroy"!==b&&a.removeEventListener(b,f,!1),delete e[b]}function vb(a,b){var d=a.ng339,c=d&&gb[d];c&&(b?delete c.data[b]:(c.handle&&(c.events.$destroy&&c.handle({},"$destroy"),Nc(a)),delete gb[d],a.ng339=u))}function wb(a,b){var d=a.ng339,d=d&&gb[d];b&&!d&&(a.ng339=d=++Ff,d=gb[d]={events:{},data:{},handle:u});return d}function Wb(a,b,d){if(Kc(a)){var c=y(d),e=!c&&b&&!H(b),f=!b;a=(a=wb(a,
!e))&&a.data;if(c)a[b]=d;else{if(f)return a;if(e)return a&&a[b];M(a,b)}}}function yb(a,b){return a.getAttribute?-1<(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+b+" "):!1}function zb(a,b){b&&a.setAttribute&&n(b.split(" "),function(b){a.setAttribute("class",U((" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+U(b)+" "," ")))})}function Ab(a,b){if(b&&a.setAttribute){var d=(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");n(b.split(" "),
function(a){a=U(a);-1===d.indexOf(" "+a+" ")&&(d+=a+" ")});a.setAttribute("class",U(d))}}function Mc(a,b){if(b)if(b.nodeType)a[a.length++]=b;else{var d=b.length;if("number"===typeof d&&b.window!==b){if(d)for(var c=0;c<d;c++)a[a.length++]=b[c]}else a[a.length++]=b}}function Oc(a,b){return Bb(a,"$"+(b||"ngController")+"Controller")}function Bb(a,b,d){9==a.nodeType&&(a=a.documentElement);for(b=I(b)?b:[b];a;){for(var c=0,e=b.length;c<e;c++)if(y(d=B.data(a,b[c])))return d;a=a.parentNode||11===a.nodeType&&
a.host}}function Pc(a){for(ub(a,!0);a.firstChild;)a.removeChild(a.firstChild)}function Xb(a,b){b||ub(a);var d=a.parentNode;d&&d.removeChild(a)}function Gf(a,b){b=b||S;if("complete"===b.document.readyState)b.setTimeout(a);else B(b).on("load",a)}function Qc(a,b){var d=Cb[b.toLowerCase()];return d&&Rc[ta(a)]&&d}function Hf(a,b){var d=function(c,d){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=b[d||c.type],g=f?f.length:0;if(g){if(q(c.immediatePropagationStopped)){var h=c.stopImmediatePropagation;
c.stopImmediatePropagation=function(){c.immediatePropagationStopped=!0;c.stopPropagation&&c.stopPropagation();h&&h.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};var k=f.specialHandlerWrapper||If;1<g&&(f=ia(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||k(a,c,f[l])}};d.elem=a;return d}function If(a,b,d){d.call(a,b)}function Jf(a,b,d){var c=b.relatedTarget;c&&(c===a||Kf.call(a,c))||d.call(a,b)}function xf(){this.$get=function(){return M(N,
{hasClass:function(a,b){a.attr&&(a=a[0]);return yb(a,b)},addClass:function(a,b){a.attr&&(a=a[0]);return Ab(a,b)},removeClass:function(a,b){a.attr&&(a=a[0]);return zb(a,b)}})}}function Ca(a,b){var d=a&&a.$$hashKey;if(d)return"function"===typeof d&&(d=a.$$hashKey()),d;d=typeof a;return d="function"==d||"object"==d&&null!==a?a.$$hashKey=d+":"+(b||Td)():d+":"+a}function Sa(a,b){if(b){var d=0;this.nextUid=function(){return++d}}n(a,this.put,this)}function Lf(a){return(a=a.toString().replace(Sc,"").match(Tc))?
"function("+(a[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function eb(a,b){function d(a){return function(b,c){if(H(b))n(b,pc(a));else return a(b,c)}}function c(a,b){Ra(a,"service");if(z(b)||I(b))b=t.instantiate(b);if(!b.$get)throw Da("pget",a);return r[a+"Provider"]=b}function e(a,b){return function(){var c=v.invoke(b,this);if(q(c))throw Da("undef",a);return c}}function f(a,b,d){return c(a,{$get:!1!==d?e(a,b):b})}function g(a){qb(q(a)||I(a),"modulesToLoad","not an array");var b=[],c;n(a,function(a){function d(a){var b,
c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=t.get(e[0]);f[e[1]].apply(f,e[2])}}if(!m.get(a)){m.put(a,!0);try{E(a)?(c=Sb(a),b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):z(a)?b.push(t.invoke(a)):I(a)?b.push(t.invoke(a)):Qa(a,"module")}catch(e){throw I(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Da("modulerr",a,e.stack||e.message||e);}}});return b}function h(a,c){function d(b,e){if(a.hasOwnProperty(b)){if(a[b]===
k)throw Da("cdep",b+" <- "+l.join(" <- "));return a[b]}try{return l.unshift(b),a[b]=k,a[b]=c(b,e)}catch(f){throw a[b]===k&&delete a[b],f;}finally{l.shift()}}function e(a,c,f,g){"string"===typeof f&&(g=f,f=null);var h=[],k=eb.$$annotate(a,b,g),l,m,t;m=0;for(l=k.length;m<l;m++){t=k[m];if("string"!==typeof t)throw Da("itkn",t);h.push(f&&f.hasOwnProperty(t)?f[t]:d(t,g))}I(a)&&(a=a[l]);return a.apply(c,h)}return{invoke:e,instantiate:function(a,b,c){var d=Object.create((I(a)?a[a.length-1]:a).prototype||
null);a=e(a,d,b,c);return H(a)||z(a)?a:d},get:d,annotate:eb.$$annotate,has:function(b){return r.hasOwnProperty(b+"Provider")||a.hasOwnProperty(b)}}}b=!0===b;var k={},l=[],m=new Sa([],!0),r={$provide:{provider:d(c),factory:d(f),service:d(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:d(function(a,b){return f(a,na(b),!1)}),constant:d(function(a,b){Ra(a,"constant");r[a]=b;A[a]=b}),decorator:function(a,b){var c=t.get(a+"Provider"),d=c.$get;c.$get=function(){var a=
v.invoke(d,c);return v.invoke(b,null,{$delegate:a})}}}},t=r.$injector=h(r,function(a,b){fa.isString(b)&&l.push(b);throw Da("unpr",l.join(" <- "));}),A={},v=A.$injector=h(A,function(a,b){var c=t.get(a+"Provider",b);return v.invoke(c.$get,c,u,a)});n(g(a),function(a){a&&v.invoke(a)});return v}function Qe(){var a=!0;this.disableAutoScrolling=function(){a=!1};this.$get=["$window","$location","$rootScope",function(b,d,c){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===ta(a))return b=
a,!0});return b}function f(a){if(a){a.scrollIntoView();var c;c=g.yOffset;z(c)?c=c():Nb(c)?(c=c[0],c="fixed"!==b.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):Q(c)||(c=0);c&&(a=a.getBoundingClientRect().top,b.scrollBy(0,a-c))}else b.scrollTo(0,0)}function g(a){a=E(a)?a:d.hash();var b;a?(b=h.getElementById(a))?f(b):(b=e(h.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var h=b.document;a&&c.$watch(function(){return d.hash()},function(a,b){a===b&&""===a||Gf(function(){c.$evalAsync(g)})});
return g}]}function hb(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;I(a)&&(a=a.join(" "));I(b)&&(b=b.join(" "));return a+" "+b}function Mf(a){E(a)&&(a=a.split(" "));var b=$();n(a,function(a){a.length&&(b[a]=!0)});return b}function Ea(a){return H(a)?a:{}}function Nf(a,b,d,c){function e(a){try{a.apply(null,ra.call(arguments,1))}finally{if(v--,0===v)for(;T.length;)try{T.pop()()}catch(b){d.error(b)}}}function f(){L=null;g();h()}function g(){a:{try{p=m.state;break a}catch(a){}p=void 0}p=q(p)?
null:p;ma(p,J)&&(p=J);J=p}function h(){if(w!==k.url()||C!==p)w=k.url(),C=p,n(aa,function(a){a(k.url(),p)})}var k=this,l=a.location,m=a.history,r=a.setTimeout,t=a.clearTimeout,A={};k.isMock=!1;var v=0,T=[];k.$$completeOutstandingRequest=e;k.$$incOutstandingRequestCount=function(){v++};k.notifyWhenNoOutstandingRequests=function(a){0===v?a():T.push(a)};var p,C,w=l.href,ga=b.find("base"),L=null;g();C=p;k.url=function(b,d,e){q(e)&&(e=null);l!==a.location&&(l=a.location);m!==a.history&&(m=a.history);if(b){var f=
C===e;if(w===b&&(!c.history||f))return k;var h=w&&Fa(w)===Fa(b);w=b;C=e;if(!c.history||h&&f){if(!h||L)L=b;d?l.replace(b):h?(d=l,e=b.indexOf("#"),e=-1===e?"":b.substr(e),d.hash=e):l.href=b;l.href!==b&&(L=b)}else m[d?"replaceState":"pushState"](e,"",b),g(),C=p;return k}return L||l.href.replace(/%27/g,"'")};k.state=function(){return p};var aa=[],D=!1,J=null;k.onUrlChange=function(b){if(!D){if(c.history)B(a).on("popstate",f);B(a).on("hashchange",f);D=!0}aa.push(b);return b};k.$$applicationDestroyed=function(){B(a).off("hashchange popstate",
f)};k.$$checkUrlChange=h;k.baseHref=function(){var a=ga.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};k.defer=function(a,b){var c;v++;c=r(function(){delete A[c];e(a)},b||0);A[c]=!0;return c};k.defer.cancel=function(a){return A[a]?(delete A[a],t(a),e(x),!0):!1}}function Ve(){this.$get=["$window","$log","$sniffer","$document",function(a,b,d,c){return new Nf(a,c,b,d)}]}function We(){this.$get=function(){function a(a,c){function e(a){a!=r&&(t?t==a&&(t=a.n):t=a,f(a.n,a.p),f(a,r),r=a,
r.n=null)}function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(a in b)throw G("$cacheFactory")("iid",a);var g=0,h=M({},c,{id:a}),k=$(),l=c&&c.capacity||Number.MAX_VALUE,m=$(),r=null,t=null;return b[a]={put:function(a,b){if(!q(b)){if(l<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}a in k||g++;k[a]=b;g>l&&this.remove(t.key);return b}},get:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;e(b)}return k[a]},remove:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;b==r&&(r=b.p);b==t&&
(t=b.n);f(b.n,b.p);delete m[a]}a in k&&(delete k[a],g--)},removeAll:function(){k=$();g=0;m=$();r=t=null},destroy:function(){m=h=k=null;delete b[a]},info:function(){return M({},h,{size:g})}}}var b={};a.info=function(){var a={};n(b,function(b,e){a[e]=b.info()});return a};a.get=function(a){return b[a]};return a}}function rf(){this.$get=["$cacheFactory",function(a){return a("templates")}]}function Cc(a,b){function d(a,b,c){var d=/^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/,e={};n(a,function(a,f){var g=a.match(d);
if(!g)throw ha("iscp",b,f,a,c?"controller bindings definition":"isolate scope definition");e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f}});return e}function c(a){var b=a.charAt(0);if(!b||b!==F(b))throw ha("baddir",a);if(a!==a.trim())throw ha("baddir",a);}var e={},f=/^\s*directive\:\s*([\w\-]+)\s+(.*)$/,g=/(([\w\-]+)(?:\:([^;]+))?;?)/,h=Wd("ngSrc,ngSrcset,src,srcset"),k=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,l=/^(on[a-z]+|formaction)$/;this.directive=function t(b,f){Ra(b,"directive");
E(b)?(c(b),qb(f,"directiveFactory"),e.hasOwnProperty(b)||(e[b]=[],a.factory(b+"Directive",["$injector","$exceptionHandler",function(a,c){var f=[];n(e[b],function(e,g){try{var h=a.invoke(e);z(h)?h={compile:na(h)}:!h.compile&&h.link&&(h.compile=na(h.link));h.priority=h.priority||0;h.index=g;h.name=h.name||b;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"EA";var k=h,l=h,m=h.name,t={isolateScope:null,bindToController:null};H(l.scope)&&(!0===l.bindToController?(t.bindToController=d(l.scope,
m,!0),t.isolateScope={}):t.isolateScope=d(l.scope,m,!1));H(l.bindToController)&&(t.bindToController=d(l.bindToController,m,!0));if(H(t.bindToController)){var v=l.controller,R=l.controllerAs;if(!v)throw ha("noctrl",m);var V;a:if(R&&E(R))V=R;else{if(E(v)){var n=Uc.exec(v);if(n){V=n[3];break a}}V=void 0}if(!V)throw ha("noident",m);}var s=k.$$bindings=t;H(s.isolateScope)&&(h.$$isolateBindings=s.isolateScope);h.$$moduleName=e.$$moduleName;f.push(h)}catch(u){c(u)}});return f}])),e[b].push(f)):n(b,pc(t));
return this};this.aHrefSanitizationWhitelist=function(a){return y(a)?(b.aHrefSanitizationWhitelist(a),this):b.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(a){return y(a)?(b.imgSrcSanitizationWhitelist(a),this):b.imgSrcSanitizationWhitelist()};var m=!0;this.debugInfoEnabled=function(a){return y(a)?(m=a,this):m};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri",function(a,
b,c,d,p,C,w,ga,L,aa,D){function J(a,b){try{a.addClass(b)}catch(c){}}function K(a,b,c,d,e){a instanceof B||(a=B(a));n(a,function(b,c){b.nodeType==Na&&b.nodeValue.match(/\S+/)&&(a[c]=B(b).wrap("<span></span>").parent()[0])});var f=O(a,b,a,c,d,e);K.$$addScopeClass(a);var g=null;return function(b,c,d){qb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=
d&&d[0])?"foreignobject"!==ta(d)&&d.toString().match(/SVG/)?"svg":"html":"html");d="html"!==g?B(Yb(g,B("<div>").append(a).html())):c?Pa.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);K.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);return d}}function O(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,t,w,D;if(p)for(D=Array(c.length),m=0;m<h.length;m+=3)f=h[m],D[f]=c[f];else D=c;m=0;for(t=h.length;m<t;)k=D[h[m++]],c=h[m++],f=h[m++],c?(c.scope?(l=a.$new(),K.$$addScopeInfo(B(k),
l)):l=a,w=c.transcludeOnThisElement?R(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?R(a,b):null,c(f,l,k,d,w)):f&&f(a,k.childNodes,u,e)}for(var h=[],k,l,m,t,p,w=0;w<a.length;w++){k=new fa;l=V(a[w],[],k,0===w?d:u,e);(f=l.length?Z(l,a[w],k,b,c,null,[],[],f):null)&&f.scope&&K.$$addScopeClass(k.$$element);k=f&&f.terminal||!(m=a[w].childNodes)||!m.length?null:O(m,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||k)h.push(w,f,k),t=!0,p=p||f;f=null}return t?g:null}function R(a,
b,c){return function(d,e,f,g,h){d||(d=a.$new(!1,h),d.$$transcluded=!0);return b(d,e,{parentBoundTranscludeFn:c,transcludeControllers:f,futureParentElement:g})}}function V(a,b,c,d,e){var h=c.$attr,k;switch(a.nodeType){case 1:P(b,va(ta(a)),"E",d,e);for(var l,m,t,p=a.attributes,w=0,D=p&&p.length;w<D;w++){var K=!1,A=!1;l=p[w];k=l.name;m=U(l.value);l=va(k);if(t=ka.test(l))k=k.replace(Vc,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()});(l=l.match(la))&&G(l[1])&&(K=k,A=k.substr(0,k.length-
5)+"end",k=k.substr(0,k.length-6));l=va(k.toLowerCase());h[l]=k;if(t||!c.hasOwnProperty(l))c[l]=m,Qc(a,l)&&(c[l]=!0);W(a,b,m,l,t);P(b,l,"A",d,e,K,A)}a=a.className;H(a)&&(a=a.animVal);if(E(a)&&""!==a)for(;k=g.exec(a);)l=va(k[2]),P(b,l,"C",d,e)&&(c[l]=U(k[3])),a=a.substr(k.index+k[0].length);break;case Na:if(11===Ha)for(;a.parentNode&&a.nextSibling&&a.nextSibling.nodeType===Na;)a.nodeValue+=a.nextSibling.nodeValue,a.parentNode.removeChild(a.nextSibling);N(b,a.nodeValue);break;case 8:try{if(k=f.exec(a.nodeValue))l=
va(k[1]),P(b,l,"M",d,e)&&(c[l]=U(k[2]))}catch(R){}}b.sort(Ia);return b}function Ta(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw ha("uterdir",b,c);1==a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return B(d)}function s(a,b,c){return function(d,e,f,g,h){e=Ta(e[0],b,c);return a(d,e,f,g,h)}}function Z(a,b,d,e,f,g,h,l,m){function t(a,b,c,d){if(a){c&&(a=s(a,c,d));a.require=q.require;a.directiveName=x;if(O===
q||q.$$isolateScope)a=ca(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=s(b,c,d));b.require=q.require;b.directiveName=x;if(O===q||q.$$isolateScope)b=ca(b,{isolateScope:!0});l.push(b)}}function p(a,b,c,d){var e;if(E(b)){var f=b.match(k);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;e||(d="$"+b+"Controller",e=g?c.inheritedData(d):c.data(d));if(!e&&!f)throw ha("ctreq",b,a);}else if(I(b))for(e=[],g=0,f=b.length;g<f;g++)e[g]=p(a,b[g],c,d);return e||
null}function w(a,b,c,d,e,f){var g=$(),h;for(h in d){var k=d[h],l={$scope:k===O||k.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},m=k.controller;"@"==m&&(m=b[k.name]);l=C(m,l,!0,k.controllerAs);g[k.name]=l;aa||a.data("$"+k.name+"Controller",l.instance)}return g}function D(a,c,e,f,g){function k(a,b,c){var d;Za(a)||(c=b,b=a,a=u);aa&&(d=v);c||(c=aa?V.parent():V);return g(a,b,d,c,Ta)}var m,t,A,v,C,V,Ga;b===e?(f=d,V=d.$$element):(V=B(e),f=new fa(V,d));A=c;O?t=c.$new(!0):R&&(A=c.$parent);g&&(C=k,
C.$$boundTransclude=g);T&&(v=w(V,f,C,T,t,c));O&&(K.$$addScopeInfo(V,t,!0,!(J&&(J===O||J===O.$$originalDirective))),K.$$addScopeClass(V,!0),t.$$isolateBindings=O.$$isolateBindings,(Ga=ba(c,f,t,t.$$isolateBindings,O))&&t.$on("$destroy",Ga));for(var n in v){Ga=T[n];var ga=v[n],L=Ga.$$bindings.bindToController;ga.identifier&&L&&(m=ba(A,f,ga.instance,L,Ga));var q=ga();q!==ga.instance&&(ga.instance=q,V.data("$"+Ga.name+"Controller",q),m&&m(),m=ba(A,f,ga.instance,L,Ga))}F=0;for(M=h.length;F<M;F++)m=h[F],
ea(m,m.isolateScope?t:c,V,f,m.require&&p(m.directiveName,m.require,V,v),C);var Ta=c;O&&(O.template||null===O.templateUrl)&&(Ta=t);a&&a(Ta,e.childNodes,u,g);for(F=l.length-1;0<=F;F--)m=l[F],ea(m,m.isolateScope?t:c,V,f,m.require&&p(m.directiveName,m.require,V,v),C)}m=m||{};for(var A=-Number.MAX_VALUE,R=m.newScopeDirective,T=m.controllerDirectives,O=m.newIsolateScopeDirective,J=m.templateDirective,n=m.nonTlbTranscludeDirective,ga=!1,L=!1,aa=m.hasElementTranscludeDirective,Z=d.$$element=B(b),q,x,P,Ia=
e,G,F=0,M=a.length;F<M;F++){q=a[F];var N=q.$$start,Q=q.$$end;N&&(Z=Ta(b,N,Q));P=u;if(A>q.priority)break;if(P=q.scope)q.templateUrl||(H(P)?(Ua("new/isolated scope",O||R,q,Z),O=q):Ua("new/isolated scope",O,q,Z)),R=R||q;x=q.name;!q.templateUrl&&q.controller&&(P=q.controller,T=T||$(),Ua("'"+x+"' controller",T[x],q,Z),T[x]=q);if(P=q.transclude)ga=!0,q.$$tlb||(Ua("transclusion",n,q,Z),n=q),"element"==P?(aa=!0,A=q.priority,P=Z,Z=d.$$element=B(X.createComment(" "+x+": "+d[x]+" ")),b=Z[0],Y(f,ra.call(P,0),
b),Ia=K(P,e,A,g&&g.name,{nonTlbTranscludeDirective:n})):(P=B(Vb(b)).contents(),Z.empty(),Ia=K(P,e,u,u,{needsNewScope:q.$$isolateScope||q.$$newScope}));if(q.template)if(L=!0,Ua("template",J,q,Z),J=q,P=z(q.template)?q.template(Z,d):q.template,P=ja(P),q.replace){g=q;P=Tb.test(P)?Xc(Yb(q.templateNamespace,U(P))):[];b=P[0];if(1!=P.length||1!==b.nodeType)throw ha("tplrt",x,"");Y(f,Z,b);P={$attr:{}};var Wc=V(b,[],P),W=a.splice(F+1,a.length-(F+1));(O||R)&&y(Wc,O,R);a=a.concat(Wc).concat(W);S(d,P);M=a.length}else Z.html(P);
if(q.templateUrl)L=!0,Ua("template",J,q,Z),J=q,q.replace&&(g=q),D=Of(a.splice(F,a.length-F),Z,d,f,ga&&Ia,h,l,{controllerDirectives:T,newScopeDirective:R!==q&&R,newIsolateScopeDirective:O,templateDirective:J,nonTlbTranscludeDirective:n}),M=a.length;else if(q.compile)try{G=q.compile(Z,d,Ia),z(G)?t(null,G,N,Q):G&&t(G.pre,G.post,N,Q)}catch(da){c(da,ua(Z))}q.terminal&&(D.terminal=!0,A=Math.max(A,q.priority))}D.scope=R&&!0===R.scope;D.transcludeOnThisElement=ga;D.templateOnThisElement=L;D.transclude=Ia;
m.hasElementTranscludeDirective=aa;return D}function y(a,b,c){for(var d=0,e=a.length;d<e;d++)a[d]=Ob(a[d],{$$isolateScope:b,$$newScope:c})}function P(b,d,f,g,h,k,l){if(d===h)return null;h=null;if(e.hasOwnProperty(d)){var m;d=a.get(d+"Directive");for(var p=0,w=d.length;p<w;p++)try{m=d[p],(q(g)||g>m.priority)&&-1!=m.restrict.indexOf(f)&&(k&&(m=Ob(m,{$$start:k,$$end:l})),b.push(m),h=m)}catch(D){c(D)}}return h}function G(b){if(e.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,f=c.length;d<f;d++)if(b=
c[d],b.multiElement)return!0;return!1}function S(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;n(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});n(b,function(b,f){"class"==f?(J(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function Of(a,b,c,e,f,g,h,k){var l=[],m,t,p=b[0],w=a.shift(),D=Ob(w,{templateUrl:null,
transclude:null,replace:null,$$originalDirective:w}),A=z(w.templateUrl)?w.templateUrl(b,c):w.templateUrl,K=w.templateNamespace;b.empty();d(A).then(function(d){var T,v;d=ja(d);if(w.replace){d=Tb.test(d)?Xc(Yb(K,U(d))):[];T=d[0];if(1!=d.length||1!==T.nodeType)throw ha("tplrt",w.name,A);d={$attr:{}};Y(e,b,T);var C=V(T,[],d);H(w.scope)&&y(C,!0);a=C.concat(a);S(c,d)}else T=p,b.html(d);a.unshift(D);m=Z(a,T,c,f,b,w,g,h,k);n(e,function(a,c){a==T&&(e[c]=b[0])});for(t=O(b[0].childNodes,f);l.length;){d=l.shift();
v=l.shift();var ga=l.shift(),L=l.shift(),C=b[0];if(!d.$$destroyed){if(v!==p){var q=v.className;k.hasElementTranscludeDirective&&w.replace||(C=Vb(T));Y(ga,B(v),C);J(B(C),q)}v=m.transcludeOnThisElement?R(d,m.transclude,L):L;m(t,d,C,e,v)}}l=null});return function(a,b,c,d,e){a=e;b.$$destroyed||(l?l.push(b,c,d,a):(m.transcludeOnThisElement&&(a=R(b,m.transclude,e)),m(t,b,c,d,a)))}}function Ia(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function Ua(a,
b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw ha("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),a,ua(d));}function N(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&K.$$addBindingClass(a);return function(a,c){var e=c.parent();b||K.$$addBindingClass(e);K.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function Yb(a,b){a=F(a||"html");switch(a){case "svg":case "math":var c=X.createElement("div");
c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function Q(a,b){if("srcdoc"==b)return L.HTML;var c=ta(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return L.RESOURCE_URL}function W(a,c,d,e,f){var g=Q(a,e);f=h[e]||f;var k=b(d,!0,g,f);if(k){if("multiple"===e&&"select"===ta(a))throw ha("selmulti",ua(a));c.push({priority:100,compile:function(){return{pre:function(a,c,h){c=h.$$observers||(h.$$observers=$());if(l.test(e))throw ha("nodomevents");
var m=h[e];m!==d&&(k=m&&b(m,!0,g,f),d=m);k&&(h[e]=k(a),(c[e]||(c[e]=[])).$$inter=!0,(h.$$observers&&h.$$observers[e].$$scope||a).$watch(k,function(a,b){"class"===e&&a!=b?h.$updateClass(a,b):h.$set(e,a)}))}}}})}}function Y(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]==d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=X.createDocumentFragment();a.appendChild(d);
B.hasData(d)&&(B.data(c,B.data(d)),oa?(Rb=!0,oa.cleanData([d])):delete B.cache[d[B.expando]]);d=1;for(e=b.length;d<e;d++)f=b[d],B(f).remove(),a.appendChild(f),delete b[d];b[0]=c;b.length=1}function ca(a,b){return M(function(){return a.apply(null,arguments)},a,b)}function ea(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,ua(d))}}function ba(a,c,d,e,f){var g=[];n(e,function(e,h){var k=e.attrName,l=e.optional,m,t,w,D;switch(e.mode){case "@":l||qa.call(c,k)||(d[h]=c[k]=void 0);c.$observe(k,function(a){E(a)&&
(d[h]=a)});c.$$observers[k].$$scope=a;E(c[k])&&(d[h]=b(c[k])(a));break;case "=":if(!qa.call(c,k)){if(l)break;c[k]=void 0}if(l&&!c[k])break;t=p(c[k]);D=t.literal?ma:function(a,b){return a===b||a!==a&&b!==b};w=t.assign||function(){m=d[h]=t(a);throw ha("nonassign",c[k],f.name);};m=d[h]=t(a);l=function(b){D(b,d[h])||(D(b,m)?w(a,b=d[h]):d[h]=b);return m=b};l.$stateful=!0;l=e.collection?a.$watchCollection(c[k],l):a.$watch(p(c[k],l),null,t.literal);g.push(l);break;case "&":t=c.hasOwnProperty(k)?p(c[k]):
x;if(t===x&&l)break;d[h]=function(b){return t(a,b)}}});return g.length&&function(){for(var a=0,b=g.length;a<b;++a)g[a]()}}var fa=function(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a};fa.prototype={$normalize:va,$addClass:function(a){a&&0<a.length&&aa.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&aa.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=Yc(a,b);c&&c.length&&aa.addClass(this.$$element,
c);(c=Yc(b,a))&&c.length&&aa.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=Qc(this.$$element[0],a),g=Zc[a],h=a;f?(this.$$element.prop(a,b),e=f):g&&(this[g]=b,h=g);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=zc(a,"-"));f=ta(this.$$element);if("a"===f&&"href"===a||"img"===f&&"src"===a)this[a]=b=D(b,"src"===a);else if("img"===f&&"srcset"===a){for(var f="",g=U(b),k=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,k=/\s/.test(g)?k:/(,)/,g=g.split(k),k=Math.floor(g.length/2),l=0;l<
k;l++)var m=2*l,f=f+D(U(g[m]),!0),f=f+(" "+U(g[m+1]));g=U(g[2*l]).split(/\s/);f+=D(U(g[0]),!0);2===g.length&&(f+=" "+U(g[1]));this[a]=b=f}!1!==d&&(null===b||q(b)?this.$$element.removeAttr(e):this.$$element.attr(e,b));(a=this.$$observers)&&n(a[h],function(a){try{a(b)}catch(d){c(d)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=$()),e=d[a]||(d[a]=[]);e.push(b);w.$evalAsync(function(){e.$$inter||!c.hasOwnProperty(a)||q(c[a])||b(c[a])});return function(){ab(e,b)}}};var da=b.startSymbol(),
ia=b.endSymbol(),ja="{{"==da||"}}"==ia?Ya:function(a){return a.replace(/\{\{/g,da).replace(/}}/g,ia)},ka=/^ngAttr[A-Z]/,la=/^(.+)Start$/;K.$$addBindingInfo=m?function(a,b){var c=a.data("$binding")||[];I(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:x;K.$$addBindingClass=m?function(a){J(a,"ng-binding")}:x;K.$$addScopeInfo=m?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:x;K.$$addScopeClass=m?function(a,b){J(a,b?"ng-isolate-scope":"ng-scope")}:x;return K}]}function va(a){return fb(a.replace(Vc,
""))}function Yc(a,b){var d="",c=a.split(/\s+/),e=b.split(/\s+/),f=0;a:for(;f<c.length;f++){for(var g=c[f],h=0;h<e.length;h++)if(g==e[h])continue a;d+=(0<d.length?" ":"")+g}return d}function Xc(a){a=B(a);var b=a.length;if(1>=b)return a;for(;b--;)8===a[b].nodeType&&Pf.call(a,b,1);return a}function Xe(){var a={},b=!1;this.register=function(b,c){Ra(b,"controller");H(b)?M(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector","$window",function(d,c){function e(a,b,c,d){if(!a||!H(a.$scope))throw G("$controller")("noscp",
d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,r;h=!0===h;k&&E(k)&&(r=k);if(E(f)){k=f.match(Uc);if(!k)throw Qf("ctrlfmt",f);m=k[1];r=r||k[3];f=a.hasOwnProperty(m)?a[m]:Bc(g.$scope,m,!0)||(b?Bc(c,m,!0):u);Qa(f,m,!0)}if(h)return h=(I(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),r&&e(g,r,l,m||f.name),M(function(){var a=d.invoke(f,l,g,m);a!==l&&(H(a)||z(a))&&(l=a,r&&e(g,r,l,m||f.name));return l},{instance:l,identifier:r});l=d.instantiate(f,g,m);r&&e(g,r,l,m||f.name);return l}}]}function Ye(){this.$get=
["$window",function(a){return B(a.document)}]}function Ze(){this.$get=["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function Zb(a){return H(a)?da(a)?a.toISOString():db(a):a}function df(){this.$get=function(){return function(a){if(!a)return"";var b=[];oc(a,function(a,c){null===a||q(a)||(I(a)?n(a,function(a,d){b.push(ja(c)+"="+ja(Zb(a)))}):b.push(ja(c)+"="+ja(Zb(a))))});return b.join("&")}}}function ef(){this.$get=function(){return function(a){function b(a,e,f){null===a||q(a)||
(I(a)?n(a,function(a,c){b(a,e+"["+(H(a)?c:"")+"]")}):H(a)&&!da(a)?oc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):d.push(ja(e)+"="+ja(Zb(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function $b(a,b){if(E(a)){var d=a.replace(Rf,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf($c))||(c=(c=d.match(Sf))&&Tf[c[0]].test(d));c&&(a=uc(d))}}return a}function ad(a){var b=$(),d;E(a)?n(a.split("\n"),function(a){d=a.indexOf(":");var e=F(U(a.substr(0,d)));a=U(a.substr(d+1));e&&
(b[e]=b[e]?b[e]+", "+a:a)}):H(a)&&n(a,function(a,d){var f=F(d),g=U(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function bd(a){var b;return function(d){b||(b=ad(a));return d?(d=b[F(d)],void 0===d&&(d=null),d):b}}function cd(a,b,d,c){if(z(c))return c(a,b,d);n(c,function(c){a=c(a,b,d)});return a}function cf(){var a=this.defaults={transformResponse:[$b],transformRequest:[function(a){return H(a)&&"[object File]"!==sa.call(a)&&"[object Blob]"!==sa.call(a)&&"[object FormData]"!==sa.call(a)?db(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},
post:ia(ac),put:ia(ac),patch:ia(ac)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer"},b=!1;this.useApplyAsync=function(a){return y(a)?(b=!!a,this):b};var d=!0;this.useLegacyPromiseExtensions=function(a){return y(a)?(d=!!a,this):d};var c=this.interceptors=[];this.$get=["$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector",function(e,f,g,h,k,l){function m(b){function c(a){var b=M({},a);b.data=cd(a.data,a.headers,a.status,f.transformResponse);
a=a.status;return 200<=a&&300>a?b:k.reject(b)}function e(a,b){var c,d={};n(a,function(a,e){z(a)?(c=a(b),null!=c&&(d[e]=c)):d[e]=a});return d}if(!fa.isObject(b))throw G("$http")("badreq",b);var f=M({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer},b);f.headers=function(b){var c=a.headers,d=M({},b.headers),f,g,h,c=M({},c.common,c[F(b.method)]);a:for(f in c){g=F(f);for(h in d)if(F(h)===g)continue a;d[f]=c[f]}return e(d,ia(b))}(b);
f.method=sb(f.method);f.paramSerializer=E(f.paramSerializer)?l.get(f.paramSerializer):f.paramSerializer;var g=[function(b){var d=b.headers,e=cd(b.data,bd(d),u,b.transformRequest);q(e)&&n(d,function(a,b){"content-type"===F(b)&&delete d[b]});q(b.withCredentials)&&!q(a.withCredentials)&&(b.withCredentials=a.withCredentials);return r(b,e).then(c,c)},u],h=k.when(f);for(n(v,function(a){(a.request||a.requestError)&&g.unshift(a.request,a.requestError);(a.response||a.responseError)&&g.push(a.response,a.responseError)});g.length;){b=
g.shift();var m=g.shift(),h=h.then(b,m)}d?(h.success=function(a){Qa(a,"fn");h.then(function(b){a(b.data,b.status,b.headers,f)});return h},h.error=function(a){Qa(a,"fn");h.then(null,function(b){a(b.data,b.status,b.headers,f)});return h}):(h.success=dd("success"),h.error=dd("error"));return h}function r(c,d){function g(a,c,d,e){function f(){l(c,a,d,e)}J&&(200<=a&&300>a?J.put(R,[a,c,ad(d),e]):J.remove(R));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function l(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?n.resolve:
n.reject)({data:a,status:b,headers:bd(d),config:c,statusText:e})}function r(a){l(a.data,a.status,ia(a.headers()),a.statusText)}function v(){var a=m.pendingRequests.indexOf(c);-1!==a&&m.pendingRequests.splice(a,1)}var n=k.defer(),D=n.promise,J,K,O=c.headers,R=t(c.url,c.paramSerializer(c.params));m.pendingRequests.push(c);D.then(v,v);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(J=H(c.cache)?c.cache:H(a.cache)?a.cache:A);J&&(K=J.get(R),y(K)?K&&z(K.then)?K.then(r,r):I(K)?l(K[1],
K[0],ia(K[2]),K[3]):l(K,200,{},"OK"):J.put(R,D));q(K)&&((K=ed(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:u)&&(O[c.xsrfHeaderName||a.xsrfHeaderName]=K),e(c.method,R,d,g,O,c.timeout,c.withCredentials,c.responseType));return D}function t(a,b){0<b.length&&(a+=(-1==a.indexOf("?")?"?":"&")+b);return a}var A=g("$http");a.paramSerializer=E(a.paramSerializer)?l.get(a.paramSerializer):a.paramSerializer;var v=[];n(c,function(a){v.unshift(E(a)?l.get(a):l.invoke(a))});m.pendingRequests=[];(function(a){n(arguments,
function(a){m[a]=function(b,c){return m(M({},c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){n(arguments,function(a){m[a]=function(b,c,d){return m(M({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");m.defaults=a;return m}]}function gf(){this.$get=function(){return function(){return new S.XMLHttpRequest}}}function ff(){this.$get=["$browser","$window","$document","$xhrFactory",function(a,b,d,c){return Uf(a,c,a.defer,b.angular.callbacks,d[0])}]}function Uf(a,b,d,
c,e){function f(a,b,d){var f=e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f.async=!0;m=function(a){f.removeEventListener("load",m,!1);f.removeEventListener("error",m,!1);e.body.removeChild(f);f=null;var g=-1,A="unknown";a&&("load"!==a.type||c[b].called||(a={type:"error"}),A=a.type,g="error"===a.type?404:200);d&&d(g,A)};f.addEventListener("load",m,!1);f.addEventListener("error",m,!1);e.body.appendChild(f);return m}return function(e,h,k,l,m,r,t,A){function v(){C&&C();w&&w.abort()}
function T(b,c,e,f,g){y(L)&&d.cancel(L);C=w=null;b(c,e,f,g);a.$$completeOutstandingRequest(x)}a.$$incOutstandingRequestCount();h=h||a.url();if("jsonp"==F(e)){var p="_"+(c.counter++).toString(36);c[p]=function(a){c[p].data=a;c[p].called=!0};var C=f(h.replace("JSON_CALLBACK","angular.callbacks."+p),p,function(a,b){T(l,a,c[p].data,"",b);c[p]=x})}else{var w=b(e,h);w.open(e,h,!0);n(m,function(a,b){y(a)&&w.setRequestHeader(b,a)});w.onload=function(){var a=w.statusText||"",b="response"in w?w.response:w.responseText,
c=1223===w.status?204:w.status;0===c&&(c=b?200:"file"==wa(h).protocol?404:0);T(l,c,b,w.getAllResponseHeaders(),a)};e=function(){T(l,-1,null,null,"")};w.onerror=e;w.onabort=e;t&&(w.withCredentials=!0);if(A)try{w.responseType=A}catch(ga){if("json"!==A)throw ga;}w.send(q(k)?null:k)}if(0<r)var L=d(v,r);else r&&z(r.then)&&r.then(v)}}function af(){var a="{{",b="}}";this.startSymbol=function(b){return b?(a=b,this):a};this.endSymbol=function(a){return a?(b=a,this):b};this.$get=["$parse","$exceptionHandler",
"$sce",function(d,c,e){function f(a){return"\\\\\\"+a}function g(c){return c.replace(m,a).replace(r,b)}function h(f,h,m,r){function p(a){try{var b=a;a=m?e.getTrusted(m,b):e.valueOf(b);var d;if(r&&!y(a))d=a;else if(null==a)d="";else{switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=db(a)}d=a}return d}catch(g){c(Ja.interr(f,g))}}r=!!r;for(var C,w,n=0,L=[],s=[],D=f.length,J=[],K=[];n<D;)if(-1!=(C=f.indexOf(a,n))&&-1!=(w=f.indexOf(b,C+k)))n!==C&&J.push(g(f.substring(n,C))),n=f.substring(C+
k,w),L.push(n),s.push(d(n,p)),n=w+l,K.push(J.length),J.push("");else{n!==D&&J.push(g(f.substring(n)));break}m&&1<J.length&&Ja.throwNoconcat(f);if(!h||L.length){var O=function(a){for(var b=0,c=L.length;b<c;b++){if(r&&q(a[b]))return;J[K[b]]=a[b]}return J.join("")};return M(function(a){var b=0,d=L.length,e=Array(d);try{for(;b<d;b++)e[b]=s[b](a);return O(e)}catch(g){c(Ja.interr(f,g))}},{exp:f,expressions:L,$$watchDelegate:function(a,b){var c;return a.$watchGroup(s,function(d,e){var f=O(d);z(b)&&b.call(this,
f,d!==e?c:f,a);c=f})}})}}var k=a.length,l=b.length,m=new RegExp(a.replace(/./g,f),"g"),r=new RegExp(b.replace(/./g,f),"g");h.startSymbol=function(){return a};h.endSymbol=function(){return b};return h}]}function bf(){this.$get=["$rootScope","$window","$q","$$q",function(a,b,d,c){function e(e,h,k,l){var m=4<arguments.length,r=m?ra.call(arguments,4):[],t=b.setInterval,A=b.clearInterval,v=0,n=y(l)&&!l,p=(n?c:d).defer(),C=p.promise;k=y(k)?k:0;C.then(null,null,m?function(){e.apply(null,r)}:e);C.$$intervalId=
t(function(){p.notify(v++);0<k&&v>=k&&(p.resolve(v),A(C.$$intervalId),delete f[C.$$intervalId]);n||a.$apply()},h);f[C.$$intervalId]=p;return C}var f={};e.cancel=function(a){return a&&a.$$intervalId in f?(f[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),delete f[a.$$intervalId],!0):!1};return e}]}function bc(a){a=a.split("/");for(var b=a.length;b--;)a[b]=ob(a[b]);return a.join("/")}function fd(a,b){var d=wa(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=ea(d.port)||Vf[d.protocol]||
null}function gd(a,b){var d="/"!==a.charAt(0);d&&(a="/"+a);var c=wa(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=xc(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!=b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function pa(a,b){if(0===b.indexOf(a))return b.substr(a.length)}function Fa(a){var b=a.indexOf("#");return-1==b?a:a.substr(0,b)}function ib(a){return a.replace(/(#.+)|#$/,"$1")}function cc(a,b,d){this.$$html5=!0;d=d||"";
fd(a,this);this.$$parse=function(a){var d=pa(b,a);if(!E(d))throw Db("ipthprfx",a,b);gd(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Qb(this.$$search),d=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(a?"?"+a:"")+d;this.$$absUrl=b+this.$$url.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;y(f=pa(a,c))?(g=f,g=y(f=pa(d,f))?b+(pa("/",f)||f):a+g):y(f=pa(b,c))?g=b+f:b==c+"/"&&(g=b);g&&this.$$parse(g);
return!!g}}function dc(a,b,d){fd(a,this);this.$$parse=function(c){var e=pa(a,c)||pa(b,c),f;q(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",q(e)&&(a=c,this.replace())):(f=pa(d,e),q(f)&&(f=e));gd(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;0===f.indexOf(e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Qb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url?
d+this.$$url:"")};this.$$parseLinkUrl=function(b,d){return Fa(a)==Fa(b)?(this.$$parse(b),!0):!1}}function hd(a,b,d){this.$$html5=!0;dc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a==Fa(c)?f=c:(g=pa(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Qb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url}}function Eb(a){return function(){return this[a]}}
function id(a,b){return function(d){if(q(d))return this[a];this[a]=b(d);this.$$compose();return this}}function hf(){var a="",b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return y(b)?(a=b,this):a};this.html5Mode=function(a){return $a(a)?(b.enabled=a,this):H(a)?($a(a.enabled)&&(b.enabled=a.enabled),$a(a.requireBase)&&(b.requireBase=a.requireBase),$a(a.rewriteLinks)&&(b.rewriteLinks=a.rewriteLinks),this):b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",
function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state=f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,b)}var l,m;m=c.baseHref();var r=c.url(),t;if(b.enabled){if(!m&&b.requireBase)throw Db("nobase");t=r.substring(0,r.indexOf("/",r.indexOf("//")+2))+(m||"/");m=e.history?cc:hd}else t=Fa(r),m=dc;var A=t.substr(0,Fa(t).lastIndexOf("/")+1);l=new m(t,A,"#"+a);l.$$parseLinkUrl(r,r);l.$$state=
c.state();var v=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(b.rewriteLinks&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!=a.which&&2!=a.button){for(var e=B(a.target);"a"!==ta(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return;var h=e.prop("href"),k=e.attr("href")||e.attr("xlink:href");H(h)&&"[object SVGAnimatedString]"===h.toString()&&(h=wa(h.animVal).href);v.test(h)||!h||e.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(h,k)||(a.preventDefault(),l.absUrl()!=c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]=
!0))}});ib(l.absUrl())!=ib(r)&&c.url(l.absUrl(),!0);var n=!0;c.onUrlChange(function(a,b){q(pa(A,a))?g.location.href=a:(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=ib(a);l.$$parse(a);l.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(n=!1,k(c,e)))}),d.$$phase||d.$digest())});d.$watch(function(){var a=ib(c.url()),b=ib(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(n||
m)n=!1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:l.$$state),k(a,f)))});l.$$replace=!1});return l}]}function jf(){var a=!0,b=this;this.debugEnabled=function(b){return y(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&
(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||x;a=!1;try{a=!!e.apply}catch(k){}return a?function(){var a=[];n(arguments,function(b){a.push(c(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Va(a,b){if("__defineGetter__"===a||"__defineSetter__"===a||"__lookupGetter__"===a||"__lookupSetter__"===
a||"__proto__"===a)throw ba("isecfld",b);return a}function jd(a,b){a+="";if(!E(a))throw ba("iseccst",b);return a}function xa(a,b){if(a){if(a.constructor===a)throw ba("isecfn",b);if(a.window===a)throw ba("isecwindow",b);if(a.children&&(a.nodeName||a.prop&&a.attr&&a.find))throw ba("isecdom",b);if(a===Object)throw ba("isecobj",b);}return a}function kd(a,b){if(a){if(a.constructor===a)throw ba("isecfn",b);if(a===Wf||a===Xf||a===Yf)throw ba("isecff",b);}}function ld(a,b){if(a&&(a===(0).constructor||a===
(!1).constructor||a==="".constructor||a==={}.constructor||a===[].constructor||a===Function.constructor))throw ba("isecaf",b);}function Zf(a,b){return"undefined"!==typeof a?a:b}function md(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function W(a,b){var d,c;switch(a.type){case s.Program:d=!0;n(a.body,function(a){W(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:W(a.argument,b);a.constant=a.argument.constant;
a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:W(a.left,b);W(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:W(a.left,b);W(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:W(a.test,b);W(a.alternate,b);W(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant=
!1;a.toWatch=[a];break;case s.MemberExpression:W(a.object,b);a.computed&&W(a.property,b);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=a.filter?!b(a.callee.name).$stateful:!1;c=[];n(a.arguments,function(a){W(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=a.filter&&!b(a.callee.name).$stateful?c:[a];break;case s.AssignmentExpression:W(a.left,b);W(a.right,b);a.constant=a.left.constant&&a.right.constant;
a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];n(a.elements,function(a){W(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=c;break;case s.ObjectExpression:d=!0;c=[];n(a.properties,function(a){W(a.value,b);d=d&&a.value.constant;a.value.constant||c.push.apply(c,a.value.toWatch)});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1,a.toWatch=[]}}function nd(a){if(1==a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:u}}
function od(a){return a.type===s.Identifier||a.type===s.MemberExpression}function pd(a){if(1===a.body.length&&od(a.body[0].expression))return{type:s.AssignmentExpression,left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function qd(a){return 0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression)}function rd(a,b){this.astBuilder=a;this.$filter=b}function sd(a,
b){this.astBuilder=a;this.$filter=b}function Fb(a){return"constructor"==a}function ec(a){return z(a.valueOf)?a.valueOf():$f.call(a)}function kf(){var a=$(),b=$();this.$get=["$filter",function(d){function c(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=ec(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function e(a,b,d,e,f){var g=e.inputs,h;if(1===g.length){var k=c,g=g[0];return a.$watch(function(a){var b=g(a);c(b,k)||(h=e(a,u,u,[b]),k=b&&ec(b));return h},b,d,f)}for(var l=[],m=[],r=0,n=
g.length;r<n;r++)l[r]=c,m[r]=null;return a.$watch(function(a){for(var b=!1,d=0,f=g.length;d<f;d++){var k=g[d](a);if(b||(b=!c(k,l[d])))m[d]=k,l[d]=k&&ec(k)}b&&(h=e(a,u,u,m));return h},b,d,f)}function f(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;z(b)&&b.apply(this,arguments);y(a)&&d.$$postDigest(function(){y(f)&&e()})},c)}function g(a,b,c,d){function e(a){var b=!0;n(a,function(a){y(a)||(b=!1)});return b}var f,g;return f=a.$watch(function(a){return d(a)},function(a,
c,d){g=a;z(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(g)&&f()})},c)}function h(a,b,c,d){var e;return e=a.$watch(function(a){return d(a)},function(a,c,d){z(b)&&b.apply(this,arguments);e()},c)}function k(a,b){if(!b)return a;var c=a.$$watchDelegate,d=!1,c=c!==g&&c!==f?function(c,e,f,g){f=d&&g?g[0]:a(c,e,f,g);return b(f,c,e)}:function(c,d,e,f){e=a(c,d,e,f);c=b(e,c,d);return y(e)?c:e};a.$$watchDelegate&&a.$$watchDelegate!==e?c.$$watchDelegate=a.$$watchDelegate:b.$stateful||(c.$$watchDelegate=
e,d=!a.inputs,c.inputs=a.inputs?a.inputs:[a]);return c}var l=Ba().noUnsafeEval,m={csp:l,expensiveChecks:!1},r={csp:l,expensiveChecks:!0};return function(c,l,v){var n,p,q;switch(typeof c){case "string":q=c=c.trim();var w=v?b:a;n=w[q];n||(":"===c.charAt(0)&&":"===c.charAt(1)&&(p=!0,c=c.substring(2)),v=v?r:m,n=new fc(v),n=(new gc(n,d,v)).parse(c),n.constant?n.$$watchDelegate=h:p?n.$$watchDelegate=n.literal?g:f:n.inputs&&(n.$$watchDelegate=e),w[q]=n);return k(n,l);case "function":return k(c,l);default:return x}}}]}
function mf(){this.$get=["$rootScope","$exceptionHandler",function(a,b){return td(function(b){a.$evalAsync(b)},b)}]}function nf(){this.$get=["$browser","$exceptionHandler",function(a,b){return td(function(b){a.defer(b)},b)}]}function td(a,b){function d(a,b,c){function d(b){return function(c){e||(e=!0,b.call(a,c))}}var e=!1;return[d(b),d(c)]}function c(){this.$$state={status:0}}function e(a,b){return function(c){b.call(a,c)}}function f(c){!c.processScheduled&&c.pending&&(c.processScheduled=!0,a(function(){var a,
d,e;e=c.pending;c.processScheduled=!1;c.pending=u;for(var f=0,g=e.length;f<g;++f){d=e[f][0];a=e[f][c.status];try{z(a)?d.resolve(a(c.value)):1===c.status?d.resolve(c.value):d.reject(c.value)}catch(h){d.reject(h),b(h)}}}))}function g(){this.promise=new c;this.resolve=e(this,this.resolve);this.reject=e(this,this.reject);this.notify=e(this,this.notify)}var h=G("$q",TypeError);M(c.prototype,{then:function(a,b,c){if(q(a)&&q(b)&&q(c))return this;var d=new g;this.$$state.pending=this.$$state.pending||[];
this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&f(this.$$state);return d.promise},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return l(b,!0,a)},function(b){return l(b,!1,a)},b)}});M(g.prototype,{resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(h("qcycle",a)):this.$$resolve(a))},$$resolve:function(a){var c,e;e=d(this,this.$$resolve,this.$$reject);try{if(H(a)||z(a))c=a&&a.then;z(c)?(this.promise.$$state.status=
-1,c.call(a,e[0],e[1],this.notify)):(this.promise.$$state.value=a,this.promise.$$state.status=1,f(this.promise.$$state))}catch(g){e[1](g),b(g)}},reject:function(a){this.promise.$$state.status||this.$$reject(a)},$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;f(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&d&&d.length&&a(function(){for(var a,e,f=0,g=d.length;f<g;f++){e=d[f][0];a=d[f][3];try{e.notify(z(a)?
a(c):c)}catch(h){b(h)}}})}});var k=function(a,b){var c=new g;b?c.resolve(a):c.reject(a);return c.promise},l=function(a,b,c){var d=null;try{z(c)&&(d=c())}catch(e){return k(e,!1)}return d&&z(d.then)?d.then(function(){return k(a,b)},function(a){return k(a,!1)}):k(a,b)},m=function(a,b,c,d){var e=new g;e.resolve(a);return e.promise.then(b,c,d)},r=function A(a){if(!z(a))throw h("norslvr",a);if(!(this instanceof A))return new A(a);var b=new g;a(function(a){b.resolve(a)},function(a){b.reject(a)});return b.promise};
r.defer=function(){return new g};r.reject=function(a){var b=new g;b.reject(a);return b.promise};r.when=m;r.resolve=m;r.all=function(a){var b=new g,c=0,d=I(a)?[]:{};n(a,function(a,e){c++;m(a).then(function(a){d.hasOwnProperty(e)||(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};return r}function wf(){this.$get=["$window","$timeout",function(a,b){var d=a.requestAnimationFrame||a.webkitRequestAnimationFrame,c=a.cancelAnimationFrame||a.webkitCancelAnimationFrame||
a.webkitCancelRequestAnimationFrame,e=!!d,f=e?function(a){var b=d(a);return function(){c(b)}}:function(a){var c=b(a,16.66,!1);return function(){b.cancel(c)}};f.supported=e;return f}]}function lf(){function a(a){function b(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$id=++nb;this.$$ChildScope=null}b.prototype=a;return b}var b=10,d=G("$rootScope"),c=null,e=null;this.digestTtl=function(a){arguments.length&&
(b=a);return b};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(f,g,h,k){function l(a){a.currentScope.$$destroyed=!0}function m(a){9===Ha&&(a.$$childHead&&m(a.$$childHead),a.$$nextSibling&&m(a.$$nextSibling));a.$parent=a.$$nextSibling=a.$$prevSibling=a.$$childHead=a.$$childTail=a.$root=a.$$watchers=null}function r(){this.$id=++nb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=this;this.$$destroyed=
!1;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$$isolateBindings=null}function t(a){if(w.$$phase)throw d("inprog",w.$$phase);w.$$phase=a}function A(a,b){do a.$$watchersCount+=b;while(a=a.$parent)}function v(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function s(){}function p(){for(;aa.length;)try{aa.shift()()}catch(a){g(a)}e=null}function C(){null===e&&(e=k.defer(function(){w.$apply(p)}))}r.prototype={constructor:r,
$new:function(b,c){var d;c=c||this;b?(d=new r,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=a(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(b||c!=this)&&d.$on("$destroy",l);return d},$watch:function(a,b,d,e){var f=h(a);if(f.$$watchDelegate)return f.$$watchDelegate(this,b,d,f,a);var g=this,k=g.$$watchers,l={fn:b,last:s,get:f,exp:e||a,eq:!!d};c=null;z(b)||(l.fn=x);k||
(k=g.$$watchers=[]);k.unshift(l);A(this,1);return function(){0<=ab(k,l)&&A(g,-1);c=null}},$watchGroup:function(a,b){function c(){h=!1;k?(k=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,k=!0;if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});n(a,function(a,b){var k=g.$watch(a,function(a,f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},
$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!q(e)){if(H(e))if(za(e))for(f!==r&&(f=r,n=f.length=0,l++),a=e.length,n!==a&&(l++,f.length=n=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==t&&(f=t={},n=0,l++);a=0;for(b in e)qa.call(e,b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(n++,f[b]=g,l++));if(n>a)for(b in l++,f)qa.call(e,b)||(n--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,g,k=1<b.length,l=0,m=
h(a,c),r=[],t={},p=!0,n=0;return this.$watch(m,function(){p?(p=!1,b(e,e,d)):b(e,g,d);if(k)if(H(e))if(za(e)){g=Array(e.length);for(var a=0;a<e.length;a++)g[a]=e[a]}else for(a in g={},e)qa.call(e,a)&&(g[a]=e[a]);else g=e})},$digest:function(){var a,f,h,l,m,r,n=b,A,q=[],v,C;t("$digest");k.$$checkUrlChange();this===w&&null!==e&&(k.defer.cancel(e),p());c=null;do{r=!1;for(A=this;u.length;){try{C=u.shift(),C.scope.$eval(C.expression,C.locals)}catch(aa){g(aa)}c=null}a:do{if(l=A.$$watchers)for(m=l.length;m--;)try{if(a=
l[m])if((f=a.get(A))!==(h=a.last)&&!(a.eq?ma(f,h):"number"===typeof f&&"number"===typeof h&&isNaN(f)&&isNaN(h)))r=!0,c=a,a.last=a.eq?bb(f,null):f,a.fn(f,h===s?f:h,A),5>n&&(v=4-n,q[v]||(q[v]=[]),q[v].push({msg:z(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:f,oldVal:h}));else if(a===c){r=!1;break a}}catch(y){g(y)}if(!(l=A.$$watchersCount&&A.$$childHead||A!==this&&A.$$nextSibling))for(;A!==this&&!(l=A.$$nextSibling);)A=A.$parent}while(A=l);if((r||u.length)&&!n--)throw w.$$phase=null,d("infdig",
b,q);}while(r||u.length);for(w.$$phase=null;L.length;)try{L.shift()()}catch(x){g(x)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===w&&k.$$applicationDestroyed();A(this,-this.$$watchersCount);for(var b in this.$$listenerCount)v(this,this.$$listenerCount[b],b);a&&a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=
this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=x;this.$on=this.$watch=this.$watchGroup=function(){return x};this.$$listeners={};this.$$nextSibling=null;m(this)}},$eval:function(a,b){return h(a)(this,b)},$evalAsync:function(a,b){w.$$phase||u.length||k.defer(function(){u.length&&w.$digest()});u.push({scope:this,expression:a,locals:b})},$$postDigest:function(a){L.push(a)},$apply:function(a){try{t("$apply");
try{return this.$eval(a)}finally{w.$$phase=null}}catch(b){g(b)}finally{try{w.$digest()}catch(c){throw g(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&aa.push(b);C()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,v(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,f=!1,h=
{name:a,targetScope:e,stopPropagation:function(){f=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=cb([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;l<m;l++)if(d[l])try{d[l].apply(null,k)}catch(r){g(r)}else d.splice(l,1),l--,m--;if(f)return h.currentScope=null,h;e=e.$parent}while(e);h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};
if(!this.$$listenerCount[a])return e;for(var f=cb([e],arguments,1),h,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,f)}catch(l){g(l)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var w=new r,u=w.$$asyncQueue=[],L=w.$$postDigestQueue=[],aa=w.$$applyAsyncQueue=[];return w}]}function ge(){var a=/^\s*(https?|ftp|mailto|tel|file):/,
b=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(b){return y(b)?(a=b,this):a};this.imgSrcSanitizationWhitelist=function(a){return y(a)?(b=a,this):b};this.$get=function(){return function(d,c){var e=c?b:a,f;f=wa(d).href;return""===f||f.match(e)?d:"unsafe:"+f}}}function ag(a){if("self"===a)return a;if(E(a)){if(-1<a.indexOf("***"))throw ya("iwcard",a);a=ud(a).replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+a+"$")}if(Ma(a))return new RegExp("^"+
a.source+"$");throw ya("imatcher");}function vd(a){var b=[];y(a)&&n(a,function(a){b.push(ag(a))});return b}function pf(){this.SCE_CONTEXTS=la;var a=["self"],b=[];this.resourceUrlWhitelist=function(b){arguments.length&&(a=vd(b));return a};this.resourceUrlBlacklist=function(a){arguments.length&&(b=vd(a));return b};this.$get=["$injector",function(d){function c(a,b){return"self"===a?ed(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=
new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var f=function(a){throw ya("unsafe");};d.has("$sanitize")&&(f=d.get("$sanitize"));var g=e(),h={};h[la.HTML]=e(g);h[la.CSS]=e(g);h[la.URL]=e(g);h[la.JS]=e(g);h[la.RESOURCE_URL]=e(h[la.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw ya("icontext",a,b);if(null===b||q(b)||""===b)return b;if("string"!==typeof b)throw ya("itype",
a);return new c(b)},getTrusted:function(d,e){if(null===e||q(e)||""===e)return e;var g=h.hasOwnProperty(d)?h[d]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue();if(d===la.RESOURCE_URL){var g=wa(e.toString()),r,t,n=!1;r=0;for(t=a.length;r<t;r++)if(c(a[r],g)){n=!0;break}if(n)for(r=0,t=b.length;r<t;r++)if(c(b[r],g)){n=!1;break}if(n)return e;throw ya("insecurl",e.toString());}if(d===la.HTML)return f(e);throw ya("unsafe");},valueOf:function(a){return a instanceof g?a.$$unwrapTrustedValue():a}}}]}
function of(){var a=!0;this.enabled=function(b){arguments.length&&(a=!!b);return a};this.$get=["$parse","$sceDelegate",function(b,d){if(a&&8>Ha)throw ya("iequirks");var c=ia(la);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ya);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;n(la,function(a,
b){var d=F(b);c[fb("parse_as_"+d)]=function(b){return e(a,b)};c[fb("get_trusted_"+d)]=function(b){return f(a,b)};c[fb("trust_as_"+d)]=function(b){return g(a,b)}});return c}]}function qf(){this.$get=["$window","$document",function(a,b){var d={},c=ea((/android (\d+)/.exec(F((a.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((a.navigator||{}).userAgent),f=b[0]||{},g,h=/^(Moz|webkit|ms)(?=[A-Z])/,k=f.body&&f.body.style,l=!1,m=!1;if(k){for(var r in k)if(l=h.exec(r)){g=l[0];g=g.substr(0,1).toUpperCase()+
g.substr(1);break}g||(g="WebkitOpacity"in k&&"webkit");l=!!("transition"in k||g+"Transition"in k);m=!!("animation"in k||g+"Animation"in k);!c||l&&m||(l=E(k.webkitTransition),m=E(k.webkitAnimation))}return{history:!(!a.history||!a.history.pushState||4>c||e),hasEvent:function(a){if("input"===a&&11>=Ha)return!1;if(q(d[a])){var b=f.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ba(),vendorPrefix:g,transitions:l,animations:m,android:c}}]}function sf(){this.$get=["$templateCache","$http","$q","$sce",
function(a,b,d,c){function e(f,g){e.totalPendingRequests++;E(f)&&a.get(f)||(f=c.getTrustedResourceUrl(f));var h=b.defaults&&b.defaults.transformResponse;I(h)?h=h.filter(function(a){return a!==$b}):h===$b&&(h=null);return b.get(f,{cache:a,transformResponse:h})["finally"](function(){e.totalPendingRequests--}).then(function(b){a.put(f,b.data);return b.data},function(a){if(!g)throw ha("tpload",f,a.status,a.statusText);return d.reject(a)})}e.totalPendingRequests=0;return e}]}function tf(){this.$get=["$rootScope",
"$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];n(a,function(a){var c=fa.element(a).data("$binding");c&&n(c,function(c){d?(new RegExp("(^|\\s)"+ud(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!=c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var k=a.querySelectorAll("["+g[h]+"model"+(d?"=":"*=")+'"'+b+'"]');if(k.length)return k}},getLocation:function(){return d.url()},
setLocation:function(b){b!==d.url()&&(d.url(b),a.$digest())},whenStable:function(a){b.notifyWhenNoOutstandingRequests(a)}}}]}function uf(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(a,b,d,c,e){function f(f,k,l){z(f)||(l=k,k=f,f=x);var m=ra.call(arguments,3),r=y(l)&&!l,t=(r?c:d).defer(),n=t.promise,q;q=b.defer(function(){try{t.resolve(f.apply(null,m))}catch(b){t.reject(b),e(b)}finally{delete g[n.$$timeoutId]}r||a.$apply()},k);n.$$timeoutId=q;g[q]=t;return n}var g={};
f.cancel=function(a){return a&&a.$$timeoutId in g?(g[a.$$timeoutId].reject("canceled"),delete g[a.$$timeoutId],b.defer.cancel(a.$$timeoutId)):!1};return f}]}function wa(a){Ha&&(Y.setAttribute("href",a),a=Y.href);Y.setAttribute("href",a);return{href:Y.href,protocol:Y.protocol?Y.protocol.replace(/:$/,""):"",host:Y.host,search:Y.search?Y.search.replace(/^\?/,""):"",hash:Y.hash?Y.hash.replace(/^#/,""):"",hostname:Y.hostname,port:Y.port,pathname:"/"===Y.pathname.charAt(0)?Y.pathname:"/"+Y.pathname}}function ed(a){a=
E(a)?wa(a):a;return a.protocol===wd.protocol&&a.host===wd.host}function vf(){this.$get=na(S)}function xd(a){function b(a){try{return decodeURIComponent(a)}catch(b){return a}}var d=a[0]||{},c={},e="";return function(){var a,g,h,k,l;a=d.cookie||"";if(a!==e)for(e=a,a=e.split("; "),c={},h=0;h<a.length;h++)g=a[h],k=g.indexOf("="),0<k&&(l=b(g.substring(0,k)),q(c[l])&&(c[l]=b(g.substring(k+1))));return c}}function zf(){this.$get=xd}function Jc(a){function b(d,c){if(H(d)){var e={};n(d,function(a,c){e[c]=
b(c,a)});return e}return a.factory(d+"Filter",c)}this.register=b;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];b("currency",yd);b("date",zd);b("filter",bg);b("json",cg);b("limitTo",dg);b("lowercase",eg);b("number",Ad);b("orderBy",Bd);b("uppercase",fg)}function bg(){return function(a,b,d){if(!za(a)){if(null==a)return a;throw G("filter")("notarray",a);}var c;switch(hc(b)){case "function":break;case "boolean":case "null":case "number":case "string":c=!0;case "object":b=
gg(b,d,c);break;default:return a}return Array.prototype.filter.call(a,b)}}function gg(a,b,d){var c=H(a)&&"$"in a;!0===b?b=ma:z(b)||(b=function(a,b){if(q(a))return!1;if(null===a||null===b)return a===b;if(H(b)||H(a)&&!qc(a))return!1;a=F(""+a);b=F(""+b);return-1!==a.indexOf(b)});return function(e){return c&&!H(e)?Ka(e,a.$,b,!1):Ka(e,a,b,d)}}function Ka(a,b,d,c,e){var f=hc(a),g=hc(b);if("string"===g&&"!"===b.charAt(0))return!Ka(a,b.substring(1),d,c);if(I(a))return a.some(function(a){return Ka(a,b,d,c)});
switch(f){case "object":var h;if(c){for(h in a)if("$"!==h.charAt(0)&&Ka(a[h],b,d,!0))return!0;return e?!1:Ka(a,b,d,!1)}if("object"===g){for(h in b)if(e=b[h],!z(e)&&!q(e)&&(f="$"===h,!Ka(f?a:a[h],e,d,f,f)))return!1;return!0}return d(a,b);case "function":return!1;default:return d(a,b)}}function hc(a){return null===a?"null":typeof a}function yd(a){var b=a.NUMBER_FORMATS;return function(a,c,e){q(c)&&(c=b.CURRENCY_SYM);q(e)&&(e=b.PATTERNS[1].maxFrac);return null==a?a:Cd(a,b.PATTERNS[1],b.GROUP_SEP,b.DECIMAL_SEP,
e).replace(/\u00A4/g,c)}}function Ad(a){var b=a.NUMBER_FORMATS;return function(a,c){return null==a?a:Cd(a,b.PATTERNS[0],b.GROUP_SEP,b.DECIMAL_SEP,c)}}function Cd(a,b,d,c,e){if(H(a))return"";var f=0>a;a=Math.abs(a);var g=Infinity===a;if(!g&&!isFinite(a))return"";var h=a+"",k="",l=!1,m=[];g&&(k="\u221e");if(!g&&-1!==h.indexOf("e")){var r=h.match(/([\d\.]+)e(-?)(\d+)/);r&&"-"==r[2]&&r[3]>e+1?a=0:(k=h,l=!0)}if(g||l)0<e&&1>a&&(k=a.toFixed(e),a=parseFloat(k),k=k.replace(ic,c));else{g=(h.split(ic)[1]||"").length;
q(e)&&(e=Math.min(Math.max(b.minFrac,g),b.maxFrac));a=+(Math.round(+(a.toString()+"e"+e)).toString()+"e"+-e);var g=(""+a).split(ic),h=g[0],g=g[1]||"",r=0,t=b.lgSize,n=b.gSize;if(h.length>=t+n)for(r=h.length-t,l=0;l<r;l++)0===(r-l)%n&&0!==l&&(k+=d),k+=h.charAt(l);for(l=r;l<h.length;l++)0===(h.length-l)%t&&0!==l&&(k+=d),k+=h.charAt(l);for(;g.length<e;)g+="0";e&&"0"!==e&&(k+=c+g.substr(0,e))}0===a&&(f=!1);m.push(f?b.negPre:b.posPre,k,f?b.negSuf:b.posSuf);return m.join("")}function Gb(a,b,d){var c="";
0>a&&(c="-",a=-a);for(a=""+a;a.length<b;)a="0"+a;d&&(a=a.substr(a.length-b));return c+a}function ca(a,b,d,c){d=d||0;return function(e){e=e["get"+a]();if(0<d||e>-d)e+=d;0===e&&-12==d&&(e=12);return Gb(e,b,c)}}function Hb(a,b){return function(d,c){var e=d["get"+a](),f=sb(b?"SHORT"+a:a);return c[f][e]}}function Dd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Ed(a){return function(b){var d=Dd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-
+d;b=1+Math.round(b/6048E5);return Gb(b,a)}}function jc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function zd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=ea(b[9]+b[10]),g=ea(b[9]+b[11]));h.call(a,ea(b[1]),ea(b[2])-1,ea(b[3]));f=ea(b[4]||0)-f;g=ea(b[5]||0)-g;h=ea(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
return function(c,d,f){var g="",h=[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;E(c)&&(c=hg.test(c)?ea(c):b(c));Q(c)&&(c=new Date(c));if(!da(c)||!isFinite(c.getTime()))return c;for(;d;)(l=ig.exec(d))?(h=cb(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=vc(f,c.getTimezoneOffset()),c=Pb(c,f,!0));n(h,function(b){k=jg[b];g+=k?k(c,a.DATETIME_FORMATS,m):b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function cg(){return function(a,b){q(b)&&(b=2);return db(a,b)}}function dg(){return function(a,
b,d){b=Infinity===Math.abs(Number(b))?Number(b):ea(b);if(isNaN(b))return a;Q(a)&&(a=a.toString());if(!I(a)&&!E(a))return a;d=!d||isNaN(d)?0:ea(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?a.slice(d,d+b):0===d?a.slice(b,a.length):a.slice(Math.max(0,d+b),d)}}function Bd(a){function b(b,d){d=d?-1:1;return b.map(function(b){var c=1,h=Ya;if(z(b))h=b;else if(E(b)){if("+"==b.charAt(0)||"-"==b.charAt(0))c="-"==b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(h=a(b),h.constant))var k=h(),h=function(a){return a[k]}}return{get:h,
descending:c*d}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}return function(a,e,f){if(!za(a))return a;I(e)||(e=[e]);0===e.length&&(e=["+"]);var g=b(e,f);g.push({get:function(){return{}},descending:f?-1:1});a=Array.prototype.map.call(a,function(a,b){return{value:a,predicateValues:g.map(function(c){var e=c.get(a);c=typeof e;if(null===e)c="string",e="null";else if("string"===c)e=e.toLowerCase();else if("object"===c)a:{if("function"===typeof e.valueOf&&
(e=e.valueOf(),d(e)))break a;if(qc(e)&&(e=e.toString(),d(e)))break a;e=b}return{value:e,type:c}})}});a.sort(function(a,b){for(var c=0,d=0,e=g.length;d<e;++d){var c=a.predicateValues[d],f=b.predicateValues[d],n=0;c.type===f.type?c.value!==f.value&&(n=c.value<f.value?-1:1):n=c.type<f.type?-1:1;if(c=n*g[d].descending)break}return c});return a=a.map(function(a){return a.value})}}function La(a){z(a)&&(a={link:a});a.restrict=a.restrict||"AC";return na(a)}function Fd(a,b,d,c,e){var f=this,g=[];f.$error=
{};f.$$success={};f.$pending=u;f.$name=e(b.name||b.ngForm||"")(d);f.$dirty=!1;f.$pristine=!0;f.$valid=!0;f.$invalid=!1;f.$submitted=!1;f.$$parentForm=Ib;f.$rollbackViewValue=function(){n(g,function(a){a.$rollbackViewValue()})};f.$commitViewValue=function(){n(g,function(a){a.$commitViewValue()})};f.$addControl=function(a){Ra(a.$name,"input");g.push(a);a.$name&&(f[a.$name]=a);a.$$parentForm=f};f.$$renameControl=function(a,b){var c=a.$name;f[c]===a&&delete f[c];f[b]=a;a.$name=b};f.$removeControl=function(a){a.$name&&
f[a.$name]===a&&delete f[a.$name];n(f.$pending,function(b,c){f.$setValidity(c,null,a)});n(f.$error,function(b,c){f.$setValidity(c,null,a)});n(f.$$success,function(b,c){f.$setValidity(c,null,a)});ab(g,a);a.$$parentForm=Ib};Gd({ctrl:this,$element:a,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){var d=a[b];d&&(ab(d,c),0===d.length&&delete a[b])},$animate:c});f.$setDirty=function(){c.removeClass(a,Wa);c.addClass(a,Jb);f.$dirty=!0;f.$pristine=!1;f.$$parentForm.$setDirty()};
f.$setPristine=function(){c.setClass(a,Wa,Jb+" ng-submitted");f.$dirty=!1;f.$pristine=!0;f.$submitted=!1;n(g,function(a){a.$setPristine()})};f.$setUntouched=function(){n(g,function(a){a.$setUntouched()})};f.$setSubmitted=function(){c.addClass(a,"ng-submitted");f.$submitted=!0;f.$$parentForm.$setSubmitted()}}function kc(a){a.$formatters.push(function(b){return a.$isEmpty(b)?b:b.toString()})}function jb(a,b,d,c,e,f){var g=F(b[0].type);if(!e.android){var h=!1;b.on("compositionstart",function(a){h=!0});
b.on("compositionend",function(){h=!1;k()})}var k=function(a){l&&(f.defer.cancel(l),l=null);if(!h){var e=b.val();a=a&&a.type;"password"===g||d.ngTrim&&"false"===d.ngTrim||(e=U(e));(c.$viewValue!==e||""===e&&c.$$hasNativeValidators)&&c.$setViewValue(e,a)}};if(e.hasEvent("input"))b.on("input",k);else{var l,m=function(a,b,c){l||(l=f.defer(function(){l=null;b&&b.value===c||k(a)}))};b.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut",
m)}b.on("change",k);c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Kb(a,b){return function(d,c){var e,f;if(da(d))return d;if(E(d)){'"'==d.charAt(0)&&'"'==d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(kg.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,
mm:0,ss:0,sss:0},n(e,function(a,c){c<b.length&&(f[b[c]]=+a)}),new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0)}return NaN}}function kb(a,b,d,c){return function(e,f,g,h,k,l,m){function r(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function n(a){return y(a)&&!da(a)?d(a)||u:a}Hd(e,f,g,h);jb(e,f,g,h,k,l);var A=h&&h.$options&&h.$options.timezone,v;h.$$parserName=a;h.$parsers.push(function(a){return h.$isEmpty(a)?null:b.test(a)?(a=d(a,v),A&&(a=Pb(a,A)),a):u});h.$formatters.push(function(a){if(a&&
!da(a))throw lb("datefmt",a);if(r(a))return(v=a)&&A&&(v=Pb(v,A,!0)),m("date")(a,c,A);v=null;return""});if(y(g.min)||g.ngMin){var s;h.$validators.min=function(a){return!r(a)||q(s)||d(a)>=s};g.$observe("min",function(a){s=n(a);h.$validate()})}if(y(g.max)||g.ngMax){var p;h.$validators.max=function(a){return!r(a)||q(p)||d(a)<=p};g.$observe("max",function(a){p=n(a);h.$validate()})}}}function Hd(a,b,d,c){(c.$$hasNativeValidators=H(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{};
return c.badInput&&!c.typeMismatch?u:a})}function Id(a,b,d,c,e){if(y(c)){a=a(c);if(!a.constant)throw lb("constexpr",d,c);return a(b)}return e}function lc(a,b){a="ngClass"+a;return["$animate",function(d){function c(a,b){var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],m=0;m<b.length;m++)if(e==b[m])continue a;c.push(e)}return c}function e(a){var b=[];return I(a)?(n(a,function(a){b=b.concat(e(a))}),b):E(a)?a.split(" "):H(a)?(n(a,function(a,c){a&&(b=b.concat(c.split(" ")))}),b):a}return{restrict:"AC",
link:function(f,g,h){function k(a,b){var c=g.data("$classCounts")||$(),d=[];n(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});g.data("$classCounts",c);return d.join(" ")}function l(a){if(!0===b||f.$index%2===b){var l=e(a||[]);if(!m){var n=k(l,1);h.$addClass(n)}else if(!ma(a,m)){var q=e(m),n=c(l,q),l=c(q,l),n=k(n,1),l=k(l,-1);n&&n.length&&d.addClass(g,n);l&&l.length&&d.removeClass(g,l)}}m=ia(a)}var m;f.$watch(h[a],l,!0);h.$observe("class",function(b){l(f.$eval(h[a]))});"ngClass"!==
a&&f.$watch("$index",function(c,d){var g=c&1;if(g!==(d&1)){var l=e(f.$eval(h[a]));g===b?(g=k(l,1),h.$addClass(g)):(g=k(l,-1),h.$removeClass(g))}})}}}]}function Gd(a){function b(a,b){b&&!f[a]?(k.addClass(e,a),f[a]=!0):!b&&f[a]&&(k.removeClass(e,a),f[a]=!1)}function d(a,c){a=a?"-"+zc(a,"-"):"";b(mb+a,!0===c);b(Jd+a,!1===c)}var c=a.ctrl,e=a.$element,f={},g=a.set,h=a.unset,k=a.$animate;f[Jd]=!(f[mb]=e.hasClass(mb));c.$setValidity=function(a,e,f){q(e)?(c.$pending||(c.$pending={}),g(c.$pending,a,f)):(c.$pending&&
h(c.$pending,a,f),Kd(c.$pending)&&(c.$pending=u));$a(e)?e?(h(c.$error,a,f),g(c.$$success,a,f)):(g(c.$error,a,f),h(c.$$success,a,f)):(h(c.$error,a,f),h(c.$$success,a,f));c.$pending?(b(Ld,!0),c.$valid=c.$invalid=u,d("",null)):(b(Ld,!1),c.$valid=Kd(c.$error),c.$invalid=!c.$valid,d("",c.$valid));e=c.$pending&&c.$pending[a]?u:c.$error[a]?!1:c.$$success[a]?!0:null;d(a,e);c.$$parentForm.$setValidity(a,e,c)}}function Kd(a){if(a)for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}var lg=/^\/(.+)\/([a-z]*)$/,
F=function(a){return E(a)?a.toLowerCase():a},qa=Object.prototype.hasOwnProperty,sb=function(a){return E(a)?a.toUpperCase():a},Ha,B,oa,ra=[].slice,Pf=[].splice,mg=[].push,sa=Object.prototype.toString,rc=Object.getPrototypeOf,Aa=G("ng"),fa=S.angular||(S.angular={}),Sb,nb=0;Ha=X.documentMode;x.$inject=[];Ya.$inject=[];var I=Array.isArray,Vd=/^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/,U=function(a){return E(a)?a.trim():a},ud=function(a){return a.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,
"\\$1").replace(/\x08/g,"\\x08")},Ba=function(){if(!y(Ba.rules)){var a=X.querySelector("[ng-csp]")||X.querySelector("[data-ng-csp]");if(a){var b=a.getAttribute("ng-csp")||a.getAttribute("data-ng-csp");Ba.rules={noUnsafeEval:!b||-1!==b.indexOf("no-unsafe-eval"),noInlineStyle:!b||-1!==b.indexOf("no-inline-style")}}else{a=Ba;try{new Function(""),b=!1}catch(d){b=!0}a.rules={noUnsafeEval:b,noInlineStyle:!1}}}return Ba.rules},pb=function(){if(y(pb.name_))return pb.name_;var a,b,d=Oa.length,c,e;for(b=0;b<
d;++b)if(c=Oa[b],a=X.querySelector("["+c.replace(":","\\:")+"jq]")){e=a.getAttribute(c+"jq");break}return pb.name_=e},Oa=["ng-","data-ng-","ng:","x-ng-"],be=/[A-Z]/g,Ac=!1,Rb,Na=3,fe={full:"1.4.8",major:1,minor:4,dot:8,codeName:"ice-manipulation"};N.expando="ng339";var gb=N.cache={},Ff=1;N._data=function(a){return this.cache[a[this.expando]]||{}};var Af=/([\:\-\_]+(.))/g,Bf=/^moz([A-Z])/,xb={mouseleave:"mouseout",mouseenter:"mouseover"},Ub=G("jqLite"),Ef=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,Tb=/<|&#?\w+;/,
Cf=/<([\w:-]+)/,Df=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ka={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ka.optgroup=ka.option;ka.tbody=ka.tfoot=ka.colgroup=ka.caption=ka.thead;ka.th=ka.td;var Kf=Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&
16)},Pa=N.prototype={ready:function(a){function b(){d||(d=!0,a())}var d=!1;"complete"===X.readyState?setTimeout(b):(this.on("DOMContentLoaded",b),N(S).on("load",b))},toString:function(){var a=[];n(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?B(this[a]):B(this[this.length+a])},length:0,push:mg,sort:[].sort,splice:[].splice},Cb={};n("multiple selected checked disabled readOnly required open".split(" "),function(a){Cb[F(a)]=a});var Rc={};n("input select option textarea button form details".split(" "),
function(a){Rc[a]=!0});var Zc={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};n({data:Wb,removeData:vb,hasData:function(a){for(var b in gb[a.ng339])return!0;return!1}},function(a,b){N[b]=a});n({data:Wb,inheritedData:Bb,scope:function(a){return B.data(a,"$scope")||Bb(a.parentNode||a,["$isolateScope","$scope"])},isolateScope:function(a){return B.data(a,"$isolateScope")||B.data(a,"$isolateScopeNoTemplate")},controller:Oc,injector:function(a){return Bb(a,
"$injector")},removeAttr:function(a,b){a.removeAttribute(b)},hasClass:yb,css:function(a,b,d){b=fb(b);if(y(d))a.style[b]=d;else return a.style[b]},attr:function(a,b,d){var c=a.nodeType;if(c!==Na&&2!==c&&8!==c)if(c=F(b),Cb[c])if(y(d))d?(a[b]=!0,a.setAttribute(b,c)):(a[b]=!1,a.removeAttribute(c));else return a[b]||(a.attributes.getNamedItem(b)||x).specified?c:u;else if(y(d))a.setAttribute(b,d);else if(a.getAttribute)return a=a.getAttribute(b,2),null===a?u:a},prop:function(a,b,d){if(y(d))a[b]=d;else return a[b]},
text:function(){function a(a,d){if(q(d)){var c=a.nodeType;return 1===c||c===Na?a.textContent:""}a.textContent=d}a.$dv="";return a}(),val:function(a,b){if(q(b)){if(a.multiple&&"select"===ta(a)){var d=[];n(a.options,function(a){a.selected&&d.push(a.value||a.text)});return 0===d.length?null:d}return a.value}a.value=b},html:function(a,b){if(q(b))return a.innerHTML;ub(a,!0);a.innerHTML=b},empty:Pc},function(a,b){N.prototype[b]=function(b,c){var e,f,g=this.length;if(a!==Pc&&q(2==a.length&&a!==yb&&a!==Oc?
b:c)){if(H(b)){for(e=0;e<g;e++)if(a===Wb)a(this[e],b);else for(f in b)a(this[e],f,b[f]);return this}e=a.$dv;g=q(e)?Math.min(g,1):g;for(f=0;f<g;f++){var h=a(this[f],b,c);e=e?e+h:h}return e}for(e=0;e<g;e++)a(this[e],b,c);return this}});n({removeData:vb,on:function(a,b,d,c){if(y(c))throw Ub("onargs");if(Kc(a)){c=wb(a,!0);var e=c.events,f=c.handle;f||(f=c.handle=Hf(a,e));c=0<=b.indexOf(" ")?b.split(" "):[b];for(var g=c.length,h=function(b,c,g){var h=e[b];h||(h=e[b]=[],h.specialHandlerWrapper=c,"$destroy"===
b||g||a.addEventListener(b,f,!1));h.push(d)};g--;)b=c[g],xb[b]?(h(xb[b],Jf),h(b,u,!0)):h(b)}},off:Nc,one:function(a,b,d){a=B(a);a.on(b,function e(){a.off(b,d);a.off(b,e)});a.on(b,d)},replaceWith:function(a,b){var d,c=a.parentNode;ub(a);n(new N(b),function(b){d?c.insertBefore(b,d.nextSibling):c.replaceChild(b,a);d=b})},children:function(a){var b=[];n(a.childNodes,function(a){1===a.nodeType&&b.push(a)});return b},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,b){var d=
a.nodeType;if(1===d||11===d){b=new N(b);for(var d=0,c=b.length;d<c;d++)a.appendChild(b[d])}},prepend:function(a,b){if(1===a.nodeType){var d=a.firstChild;n(new N(b),function(b){a.insertBefore(b,d)})}},wrap:function(a,b){b=B(b).eq(0).clone()[0];var d=a.parentNode;d&&d.replaceChild(b,a);b.appendChild(a)},remove:Xb,detach:function(a){Xb(a,!0)},after:function(a,b){var d=a,c=a.parentNode;b=new N(b);for(var e=0,f=b.length;e<f;e++){var g=b[e];c.insertBefore(g,d.nextSibling);d=g}},addClass:Ab,removeClass:zb,
toggleClass:function(a,b,d){b&&n(b.split(" "),function(b){var e=d;q(e)&&(e=!yb(a,b));(e?Ab:zb)(a,b)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,b){return a.getElementsByTagName?a.getElementsByTagName(b):[]},clone:Vb,triggerHandler:function(a,b,d){var c,e,f=b.type||b,g=wb(a);if(g=(g=g&&g.events)&&g[f])c={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},
stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:x,type:f,target:a},b.type&&(c=M(c,b)),b=ia(g),e=d?[c].concat(d):[c],n(b,function(b){c.isImmediatePropagationStopped()||b.apply(a,e)})}},function(a,b){N.prototype[b]=function(b,c,e){for(var f,g=0,h=this.length;g<h;g++)q(f)?(f=a(this[g],b,c,e),y(f)&&(f=B(f))):Mc(f,a(this[g],b,c,e));return y(f)?f:this};N.prototype.bind=N.prototype.on;
N.prototype.unbind=N.prototype.off});Sa.prototype={put:function(a,b){this[Ca(a,this.nextUid)]=b},get:function(a){return this[Ca(a,this.nextUid)]},remove:function(a){var b=this[a=Ca(a,this.nextUid)];delete this[a];return b}};var yf=[function(){this.$get=[function(){return Sa}]}],Tc=/^[^\(]*\(\s*([^\)]*)\)/m,ng=/,/,og=/^\s*(_?)(\S+?)\1\s*$/,Sc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Da=G("$injector");eb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw E(d)&&
d||(d=a.name||Lf(a)),Da("strictdi",d);b=a.toString().replace(Sc,"");b=b.match(Tc);n(b[1].split(ng),function(a){a.replace(og,function(a,b,d){c.push(d)})})}a.$inject=c}}else I(a)?(b=a.length-1,Qa(a[b],"fn"),c=a.slice(0,b)):Qa(a,"fn",!0);return c};var Md=G("$animate"),Ue=function(){this.$get=["$q","$$rAF",function(a,b){function d(){}d.all=x;d.chain=x;d.prototype={end:x,cancel:x,resume:x,pause:x,complete:x,then:function(c,d){return a(function(a){b(function(){a()})}).then(c,d)}};return d}]},Te=function(){var a=
new Sa,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=E(b)?b.split(" "):I(b)?b:[],n(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){n(b,function(b){var c=a.get(b);if(c){var d=Mf(b.attr("class")),e="",f="";n(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});n(b,function(a){e&&Ab(a,e);f&&zb(a,f)});a.remove(b)}});b.length=0}return{enabled:x,on:x,off:x,pin:x,push:function(g,h,k,l){l&&l();k=k||{};k.from&&g.css(k.from);
k.to&&g.css(k.to);if(k.addClass||k.removeClass)if(h=k.addClass,l=k.removeClass,k=a.get(g)||{},h=e(k,h,!0),l=e(k,l,!1),h||l)a.put(g,k),b.push(g),1===b.length&&c.$$postDigest(f);return new d}}}]},Re=["$provide",function(a){var b=this;this.$$registeredAnimations=Object.create(null);this.register=function(d,c){if(d&&"."!==d.charAt(0))throw Md("notcsel",d);var e=d+"-animation";b.$$registeredAnimations[d.substr(1)]=e;a.factory(e,c)};this.classNameFilter=function(a){if(1===arguments.length&&(this.$$classNameFilter=
a instanceof RegExp?a:null)&&/(\s+|\/)ng-animate(\s+|\/)/.test(this.$$classNameFilter.toString()))throw Md("nongcls","ng-animate");return this.$$classNameFilter};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var h;a:{for(h=0;h<d.length;h++){var k=d[h];if(1===k.nodeType){h=k;break a}}h=void 0}!h||h.parentNode||h.previousElementSibling||(d=null)}d?d.after(a):c.prepend(a)}return{on:a.on,off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.end&&a.end()},enter:function(e,f,g,h){f=
f&&B(f);g=g&&B(g);f=f||g.parent();b(e,f,g);return a.push(e,"enter",Ea(h))},move:function(e,f,g,h){f=f&&B(f);g=g&&B(g);f=f||g.parent();b(e,f,g);return a.push(e,"move",Ea(h))},leave:function(b,c){return a.push(b,"leave",Ea(c),function(){b.remove()})},addClass:function(b,c,g){g=Ea(g);g.addClass=hb(g.addclass,c);return a.push(b,"addClass",g)},removeClass:function(b,c,g){g=Ea(g);g.removeClass=hb(g.removeClass,c);return a.push(b,"removeClass",g)},setClass:function(b,c,g,h){h=Ea(h);h.addClass=hb(h.addClass,
c);h.removeClass=hb(h.removeClass,g);return a.push(b,"setClass",h)},animate:function(b,c,g,h,k){k=Ea(k);k.from=k.from?M(k.from,c):c;k.to=k.to?M(k.to,g):g;k.tempClasses=hb(k.tempClasses,h||"ng-inline-animate");return a.push(b,"animate",k)}}}]}],Se=function(){this.$get=["$$rAF","$q",function(a,b){var d=function(){};d.prototype={done:function(a){this.defer&&this.defer[!0===a?"reject":"resolve"]()},end:function(){this.done()},cancel:function(){this.done(!0)},getPromise:function(){this.defer||(this.defer=
b.defer());return this.defer.promise},then:function(a,b){return this.getPromise().then(a,b)},"catch":function(a){return this.getPromise()["catch"](a)},"finally":function(a){return this.getPromise()["finally"](a)}};return function(b,e){function f(){a(function(){e.addClass&&(b.addClass(e.addClass),e.addClass=null);e.removeClass&&(b.removeClass(e.removeClass),e.removeClass=null);e.to&&(b.css(e.to),e.to=null);g||h.done();g=!0});return h}e.cleanupStyles&&(e.from=e.to=null);e.from&&(b.css(e.from),e.from=
null);var g,h=new d;return{start:f,end:f}}}]},ha=G("$compile");Cc.$inject=["$provide","$$sanitizeUriProvider"];var Vc=/^((?:x|data)[\:\-_])/i,Qf=G("$controller"),Uc=/^(\S+)(\s+as\s+(\w+))?$/,$e=function(){this.$get=["$document",function(a){return function(b){b?!b.nodeType&&b instanceof B&&(b=b[0]):b=a[0].body;return b.offsetWidth+1}}]},$c="application/json",ac={"Content-Type":$c+";charset=utf-8"},Sf=/^\[|^\{(?!\{)/,Tf={"[":/]$/,"{":/}$/},Rf=/^\)\]\}',?\n/,pg=G("$http"),dd=function(a){return function(){throw pg("legacy",
a);}},Ja=fa.$interpolateMinErr=G("$interpolate");Ja.throwNoconcat=function(a){throw Ja("noconcat",a);};Ja.interr=function(a,b){return Ja("interr",a,b.toString())};var qg=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,Vf={http:80,https:443,ftp:21},Db=G("$location"),rg={$$html5:!1,$$replace:!1,absUrl:Eb("$$absUrl"),url:function(a){if(q(a))return this.$$url;var b=qg.exec(a);(b[1]||""===a)&&this.path(decodeURIComponent(b[1]));(b[2]||b[1]||""===a)&&this.search(b[3]||"");this.hash(b[5]||"");return this},protocol:Eb("$$protocol"),
host:Eb("$$host"),port:Eb("$$port"),path:id("$$path",function(a){a=null!==a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,b){switch(arguments.length){case 0:return this.$$search;case 1:if(E(a)||Q(a))a=a.toString(),this.$$search=xc(a);else if(H(a))a=bb(a,{}),n(a,function(b,c){null==b&&delete a[c]}),this.$$search=a;else throw Db("isrcharg");break;default:q(b)||null===b?delete this.$$search[a]:this.$$search[a]=b}this.$$compose();return this},hash:id("$$hash",function(a){return null!==
a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};n([hd,dc,cc],function(a){a.prototype=Object.create(rg);a.prototype.state=function(b){if(!arguments.length)return this.$$state;if(a!==cc||!this.$$html5)throw Db("nostate");this.$$state=q(b)?null:b;return this}});var ba=G("$parse"),Wf=Function.prototype.call,Xf=Function.prototype.apply,Yf=Function.prototype.bind,Lb=$();n("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Lb[a]=!0});var sg={n:"\n",f:"\f",r:"\r",
t:"\t",v:"\v","'":"'",'"':'"'},fc=function(a){this.options=a};fc.prototype={constructor:fc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(a))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;
else{var b=a+this.peek(),d=b+this.peek(2),c=Lb[b],e=Lb[d];Lb[a]||c||e?(a=e?d:c?b:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,b){return-1!==b.indexOf(a)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||
"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=y(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw ba("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index<this.text.length;){var d=F(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var c=this.peek();
if("e"==d&&this.isExpOperator(c))a+=d;else if(this.isExpOperator(d)&&c&&this.isNumber(c)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||c&&this.isNumber(c)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:b,text:a,constant:!0,value:Number(a)})},readIdent:function(){for(var a=this.index;this.index<this.text.length;){var b=this.text.charAt(this.index);if(!this.isIdent(b)&&!this.isNumber(b))break;this.index++}this.tokens.push({index:a,
text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var b=this.index;this.index++;for(var d="",c=a,e=!1;this.index<this.text.length;){var f=this.text.charAt(this.index),c=c+f;if(e)"u"===f?(e=this.text.substring(this.index+1,this.index+5),e.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+e+"]"),this.index+=4,d+=String.fromCharCode(parseInt(e,16))):d+=sg[f]||f,e=!1;else if("\\"===f)e=!0;else{if(f===a){this.index++;this.tokens.push({index:b,text:c,constant:!0,
value:d});return}d+=f}this.index++}this.throwError("Unterminated quote",b)}};var s=function(a,b){this.lexer=a;this.options=b};s.Program="Program";s.ExpressionStatement="ExpressionStatement";s.AssignmentExpression="AssignmentExpression";s.ConditionalExpression="ConditionalExpression";s.LogicalExpression="LogicalExpression";s.BinaryExpression="BinaryExpression";s.UnaryExpression="UnaryExpression";s.CallExpression="CallExpression";s.MemberExpression="MemberExpression";s.Identifier="Identifier";s.Literal=
"Literal";s.ArrayExpression="ArrayExpression";s.Property="Property";s.ObjectExpression="ObjectExpression";s.ThisExpression="ThisExpression";s.NGValueParameter="NGValueParameter";s.prototype={ast:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:s.Program,
body:a}},expressionStatement:function(){return{type:s.ExpressionStatement,expression:this.filterChain()}},filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();this.expect("=")&&(a={type:s.AssignmentExpression,left:a,right:this.assignment(),operator:"="});return a},ternary:function(){var a=this.logicalOR(),b,d;return this.expect("?")&&(b=this.expression(),this.consume(":"))?
(d=this.expression(),{type:s.ConditionalExpression,test:a,alternate:b,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a={type:s.LogicalExpression,operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a={type:s.LogicalExpression,operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),b;b=this.expect("==","!=","===","!==");)a={type:s.BinaryExpression,
operator:b.text,left:a,right:this.relational()};return a},relational:function(){for(var a=this.additive(),b;b=this.expect("<",">","<=",">=");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text,
left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.constants.hasOwnProperty(this.peek().text)?a=bb(this.constants[this.consume().text]):this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():
this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(),
arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.expression());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;
a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:s.Property,kind:"init"};this.peek().constant?b.key=this.constant():this.peek().identifier?b.key=this.identifier():this.throwError("invalid key",this.peek());this.consume(":");b.value=this.expression();a.push(b)}while(this.expect(","))}this.consume("}");return{type:s.ObjectExpression,properties:a}},
throwError:function(a,b){throw ba("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw ba("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw ba("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];
var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},constants:{"true":{type:s.Literal,value:!0},"false":{type:s.Literal,value:!1},"null":{type:s.Literal,value:null},undefined:{type:s.Literal,value:u},"this":{type:s.ThisExpression}}};rd.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.state={nextId:0,filters:{},expensiveChecks:b,fn:{vars:[],body:[],own:{}},assign:{vars:[],
body:[],own:{}},inputs:[]};W(c,d.$filter);var e="",f;this.stage="assign";if(f=pd(c))this.state.computing="assign",e=this.nextId(),this.recurse(f,e),this.return_(e),e="fn.assign="+this.generateFunction("assign","s,v,l");f=nd(c.body);d.stage="inputs";n(f,function(a,b){var c="fn"+b;d.state[c]={vars:[],body:[],own:{}};d.state.computing=c;var e=d.nextId();d.recurse(a,e);d.return_(e);d.state.inputs.push(c);a.watchId=b});this.state.computing="fn";this.stage="main";this.recurse(c);e='"'+this.USE+" "+this.STRICT+
'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+e+this.watchFns()+"return fn;";e=(new Function("$filter","ensureSafeMemberName","ensureSafeObject","ensureSafeFunction","getStringValue","ensureSafeAssignContext","ifDefined","plus","text",e))(this.$filter,Va,xa,kd,jd,ld,Zf,md,a);this.state=this.stage=u;e.literal=qd(c);e.constant=c.constant;return e},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;n(b,function(b){a.push("var "+b+"="+d.generateFunction(b,
"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;n(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,b,
d,c,e,f){var g,h,k=this,l,m;c=c||x;if(!f&&y(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case s.Program:n(a.body,function(b,c){k.recurse(b.expression,u,u,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(m);break;case s.UnaryExpression:this.recurse(a.argument,u,u,function(a){h=a});m=a.operator+"("+this.ifDefined(h,
0)+")";this.assign(b,m);c(m);break;case s.BinaryExpression:this.recurse(a.left,u,u,function(a){g=a});this.recurse(a.right,u,u,function(a){h=a});m="+"===a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right,b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test,
b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);Va(a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.not(k.nonComputedMember("s",a.name)),k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s",
a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));(k.state.expensiveChecks||Fb(a.name))&&k.addEnsureSafeObject(b);c(b);break;case s.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,u,function(){k.if_(k.notNull(g),function(){if(a.computed)h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),k.addEnsureSafeMemberName(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,h),"{}")),m=k.ensureSafeObject(k.computedMember(g,
h)),k.assign(b,m),d&&(d.computed=!0,d.name=h);else{Va(a.property.name);e&&1!==e&&k.if_(k.not(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}"));m=k.nonComputedMember(g,a.property.name);if(k.state.expensiveChecks||Fb(a.property.name))m=k.ensureSafeObject(m);k.assign(b,m);d&&(d.computed=!1,d.name=a.property.name)}},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();a.filter?(h=k.filter(a.callee.name),l=[],n(a.arguments,
function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+l.join(",")+")",k.assign(b,m),c(b)):(h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){k.addEnsureSafeFunction(h);n(a.arguments,function(a){k.recurse(a,k.nextId(),u,function(a){l.push(k.ensureSafeObject(a))})});g.name?(k.state.expensiveChecks||k.addEnsureSafeObject(g.context),m=k.member(g.context,g.name,g.computed)+"("+l.join(",")+")"):m=h+"("+l.join(",")+")";m=k.ensureSafeObject(m);k.assign(b,m)},
function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=this.nextId();g={};if(!od(a.left))throw ba("lval");this.recurse(a.left,u,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);k.addEnsureSafeObject(k.member(g.context,g.name,g.computed));k.addEnsureSafeAssignContext(g.context);m=k.member(g.context,g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=[];n(a.elements,function(a){k.recurse(a,k.nextId(),u,function(a){l.push(a)})});
m="["+l.join(",")+"]";this.assign(b,m);c(m);break;case s.ObjectExpression:l=[];n(a.properties,function(a){k.recurse(a.value,k.nextId(),u,function(b){l.push(k.escape(a.key.type===s.Identifier?a.key.name:""+a.key.value)+":"+b)})});m="{"+l.join(",")+"}";this.assign(b,m);c(m);break;case s.ThisExpression:this.assign(b,"s");c("s");break;case s.NGValueParameter:this.assign(b,"v"),c("v")}},getHasOwnProperty:function(a,b){var d=a+"."+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+
this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,
"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){return a+"."+b},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,b)},addEnsureSafeObject:function(a){this.current().body.push(this.ensureSafeObject(a),";")},addEnsureSafeMemberName:function(a){this.current().body.push(this.ensureSafeMemberName(a),";")},
addEnsureSafeFunction:function(a){this.current().body.push(this.ensureSafeFunction(a),";")},addEnsureSafeAssignContext:function(a){this.current().body.push(this.ensureSafeAssignContext(a),";")},ensureSafeObject:function(a){return"ensureSafeObject("+a+",text)"},ensureSafeMemberName:function(a){return"ensureSafeMemberName("+a+",text)"},ensureSafeFunction:function(a){return"ensureSafeFunction("+a+",text)"},getStringValue:function(a){this.assign(a,"getStringValue("+a+",text)")},ensureSafeAssignContext:function(a){return"ensureSafeAssignContext("+
a+",text)"},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(E(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(Q(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"===
typeof a)return"undefined";throw ba("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};sd.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.expression=a;this.expensiveChecks=b;W(c,d.$filter);var e,f;if(e=pd(c))f=this.recurse(e);e=nd(c.body);var g;e&&(g=[],n(e,function(a,b){var c=d.recurse(a);a.input=c;g.push(c);a.watchId=b}));var h=[];n(c.body,function(a){h.push(d.recurse(a.expression))});
e=0===c.body.length?function(){}:1===c.body.length?h[0]:function(a,b){var c;n(h,function(d){c=d(a,b)});return c};f&&(e.assign=function(a,b,c){return f(a,c,b)});g&&(e.inputs=g);e.literal=qd(c);e.constant=c.constant;return e},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left),
e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return Va(a.name,f.expression),f.identifier(a.name,f.expensiveChecks||Fb(a.name),b,d,f.expression);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(Va(a.property.name,
f.expression),e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d,f.expression):this.nonComputedMember(c,e,f.expensiveChecks,b,d,f.expression);case s.CallExpression:return g=[],n(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var r=[],n=0;n<g.length;++n)r.push(g[n](a,c,d,f));a=e.apply(u,r,f);return b?{context:u,name:u,value:a}:a}:function(a,
c,d,m){var r=e(a,c,d,m),n;if(null!=r.value){xa(r.context,f.expression);kd(r.value,f.expression);n=[];for(var q=0;q<g.length;++q)n.push(xa(g[q](a,c,d,m),f.expression));n=xa(r.value.apply(r.context,n),f.expression)}return b?{value:n}:n};case s.AssignmentExpression:return c=this.recurse(a.left,!0,1),e=this.recurse(a.right),function(a,d,g,m){var n=c(a,d,g,m);a=e(a,d,g,m);xa(n.value,f.expression);ld(n.context);n.context[n.name]=a;return b?{value:a}:a};case s.ArrayExpression:return g=[],n(a.elements,function(a){g.push(f.recurse(a))}),
function(a,c,d,e){for(var f=[],n=0;n<g.length;++n)f.push(g[n](a,c,d,e));return b?{value:f}:f};case s.ObjectExpression:return g=[],n(a.properties,function(a){g.push({key:a.key.type===s.Identifier?a.key.name:""+a.key.value,value:f.recurse(a.value)})}),function(a,c,d,e){for(var f={},n=0;n<g.length;++n)f[g[n].key]=g[n].value(a,c,d,e);return b?{value:f}:f};case s.ThisExpression:return function(a){return b?{value:a}:a};case s.NGValueParameter:return function(a,c,d,e){return b?{value:d}:d}}},"unary+":function(a,
b){return function(d,c,e,f){d=a(d,c,e,f);d=y(d)?+d:0;return b?{value:d}:d}},"unary-":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=y(d)?-d:0;return b?{value:d}:d}},"unary!":function(a,b){return function(d,c,e,f){d=!a(d,c,e,f);return b?{value:d}:d}},"binary+":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);h=md(h,c);return d?{value:h}:h}},"binary-":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);h=(y(h)?h:0)-(y(c)?c:0);return d?{value:h}:h}},"binary*":function(a,
b,d){return function(c,e,f,g){c=a(c,e,f,g)*b(c,e,f,g);return d?{value:c}:c}},"binary/":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)/b(c,e,f,g);return d?{value:c}:c}},"binary%":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)%b(c,e,f,g);return d?{value:c}:c}},"binary===":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)===b(c,e,f,g);return d?{value:c}:c}},"binary!==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!==b(c,e,f,g);return d?{value:c}:c}},"binary==":function(a,b,
d){return function(c,e,f,g){c=a(c,e,f,g)==b(c,e,f,g);return d?{value:c}:c}},"binary!=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!=b(c,e,f,g);return d?{value:c}:c}},"binary<":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<b(c,e,f,g);return d?{value:c}:c}},"binary>":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,
e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:u,name:u,value:a}:a}},identifier:function(a,b,d,c,e){return function(f,g,h,k){f=
g&&a in g?g:f;c&&1!==c&&f&&!f[a]&&(f[a]={});g=f?f[a]:u;b&&xa(g,e);return d?{context:f,name:a,value:g}:g}},computedMember:function(a,b,d,c,e){return function(f,g,h,k){var l=a(f,g,h,k),m,n;null!=l&&(m=b(f,g,h,k),m=jd(m),Va(m,e),c&&1!==c&&l&&!l[m]&&(l[m]={}),n=l[m],xa(n,e));return d?{context:l,name:m,value:n}:n}},nonComputedMember:function(a,b,d,c,e,f){return function(g,h,k,l){g=a(g,h,k,l);e&&1!==e&&g&&!g[b]&&(g[b]={});h=null!=g?g[b]:u;(d||Fb(b))&&xa(h,f);return c?{context:g,name:b,value:h}:h}},inputs:function(a,
b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};var gc=function(a,b,d){this.lexer=a;this.$filter=b;this.options=d;this.ast=new s(this.lexer);this.astCompiler=d.csp?new sd(this.ast,b):new rd(this.ast,b)};gc.prototype={constructor:gc,parse:function(a){return this.astCompiler.compile(a,this.options.expensiveChecks)}};$();$();var $f=Object.prototype.valueOf,ya=G("$sce"),la={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},ha=G("$compile"),Y=X.createElement("a"),wd=wa(S.location.href);
xd.$inject=["$document"];Jc.$inject=["$provide"];yd.$inject=["$locale"];Ad.$inject=["$locale"];var ic=".",jg={yyyy:ca("FullYear",4),yy:ca("FullYear",2,0,!0),y:ca("FullYear",1),MMMM:Hb("Month"),MMM:Hb("Month",!0),MM:ca("Month",2,1),M:ca("Month",1,1),dd:ca("Date",2),d:ca("Date",1),HH:ca("Hours",2),H:ca("Hours",1),hh:ca("Hours",2,-12),h:ca("Hours",1,-12),mm:ca("Minutes",2),m:ca("Minutes",1),ss:ca("Seconds",2),s:ca("Seconds",1),sss:ca("Milliseconds",3),EEEE:Hb("Day"),EEE:Hb("Day",!0),a:function(a,b){return 12>
a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Gb(Math[0<a?"floor":"ceil"](a/60),2)+Gb(Math.abs(a%60),2))},ww:Ed(2),w:Ed(1),G:jc,GG:jc,GGG:jc,GGGG:function(a,b){return 0>=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},ig=/((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,hg=/^\-?\d+$/;zd.$inject=["$locale"];var eg=na(F),fg=na(sb);Bd.$inject=["$parse"];var he=na({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,
b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===sa.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),tb={};n(Cb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!=a){var c=va("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});tb[c]=function(){return{restrict:"A",priority:100,link:e}}}});n(Zc,function(a,b){tb[b]=function(){return{priority:100,link:function(a,
c,e){if("ngPattern"===b&&"/"==e.ngPattern.charAt(0)&&(c=e.ngPattern.match(lg))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});n(["src","srcset","href"],function(a){var b=va("ng-"+a);tb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===sa.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b),Ha&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}});
var Ib={$addControl:x,$$renameControl:function(a,b){a.$name=b},$removeControl:x,$setValidity:x,$setDirty:x,$setPristine:x,$setSubmitted:x};Fd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var Nd=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||x}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Fd,compile:function(d,f){d.addClass(Wa).addClass(mb);var g=f.name?"name":a&&f.ngForm?"ngForm":
!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in e)){var q=function(b){a.$apply(function(){n.$commitViewValue();n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",q,!1);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",q,!1)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n);var s=g?c(n.$name):x;g&&(s(a,n),e.$observe(g,function(b){n.$name!==b&&(s(a,u),n.$$parentForm.$$renameControl(n,b),s=c(n.$name),s(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n);
s(a,u);M(n,Ib)})}}}}}]},ie=Nd(),ve=Nd(!0),kg=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/,tg=/^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/,ug=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,vg=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,Od=/^(\d{4})-(\d{2})-(\d{2})$/,Pd=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,mc=/^(\d{4})-W(\d\d)$/,Qd=/^(\d{4})-(\d\d)$/,
Rd=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Sd={text:function(a,b,d,c,e,f){jb(a,b,d,c,e,f);kc(c)},date:kb("date",Od,Kb(Od,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":kb("datetimelocal",Pd,Kb(Pd,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:kb("time",Rd,Kb(Rd,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:kb("week",mc,function(a,b){if(da(a))return a;if(E(a)){mc.lastIndex=0;var d=mc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Dd(c),e=7*(e-1);b&&(d=b.getHours(),f=
b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:kb("month",Qd,Kb(Qd,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Hd(a,b,d,c);jb(a,b,d,c,e,f);c.$$parserName="number";c.$parsers.push(function(a){return c.$isEmpty(a)?null:vg.test(a)?parseFloat(a):u});c.$formatters.push(function(a){if(!c.$isEmpty(a)){if(!Q(a))throw lb("numfmt",a);a=a.toString()}return a});if(y(d.min)||d.ngMin){var g;c.$validators.min=function(a){return c.$isEmpty(a)||
q(g)||a>=g};d.$observe("min",function(a){y(a)&&!Q(a)&&(a=parseFloat(a,10));g=Q(a)&&!isNaN(a)?a:u;c.$validate()})}if(y(d.max)||d.ngMax){var h;c.$validators.max=function(a){return c.$isEmpty(a)||q(h)||a<=h};d.$observe("max",function(a){y(a)&&!Q(a)&&(a=parseFloat(a,10));h=Q(a)&&!isNaN(a)?a:u;c.$validate()})}},url:function(a,b,d,c,e,f){jb(a,b,d,c,e,f);kc(c);c.$$parserName="url";c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||tg.test(d)}},email:function(a,b,d,c,e,f){jb(a,b,d,c,e,f);kc(c);
c.$$parserName="email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||ug.test(d)}},radio:function(a,b,d,c){q(d.name)&&b.attr("name",++nb);b.on("click",function(a){b[0].checked&&c.$setViewValue(d.value,a&&a.type)});c.$render=function(){b[0].checked=d.value==c.$viewValue};d.$observe("value",c.$render)},checkbox:function(a,b,d,c,e,f,g,h){var k=Id(h,a,"ngTrueValue",d.ngTrueValue,!0),l=Id(h,a,"ngFalseValue",d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&&
a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return ma(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:x,button:x,submit:x,reset:x,file:x},Dc=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(Sd[F(g.type)]||Sd.text)(e,f,g,h[0],b,a,d,c)}}}}],wg=/^(true|false|\d+)$/,Ne=function(){return{restrict:"A",priority:100,compile:function(a,
b){return wg.test(b.ngValue)?function(a,b,e){e.$set("value",a.$eval(e.ngValue))}:function(a,b,e){a.$watch(e.ngValue,function(a){e.$set("value",a)})}}}},ne=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=q(a)?"":a})}}}}],pe=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));
b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=q(a)?"":a})}}}}],oe=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){c.html(a.getTrustedHtml(f(b))||"")})}}}}],Me=na({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),
qe=lc("",!0),se=lc("Odd",0),re=lc("Even",1),te=La({compile:function(a,b){b.$set("ngCloak",u);a.removeClass("ng-cloak")}}),ue=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Ic={},xg={blur:!0,focus:!0};n("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=va("ng-"+a);Ic[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g=
d(f[b],null,!0);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};xg[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var xe=["$animate",function(a){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(b,d,c,e,f){var g,h,k;b.$watch(c.ngIf,function(b){b?h||f(function(b,e){h=e;b[b.length++]=X.createComment(" end ngIf: "+c.ngIf+" ");g={clone:b};a.enter(b,d.parent(),d)}):(k&&(k.remove(),k=null),h&&(h.$destroy(),h=null),g&&(k=
rb(g.clone),a.leave(k).then(function(){k=null}),g=null))})}}}],ye=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:fa.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,q){var s=0,v,u,p,C=function(){u&&(u.remove(),u=null);v&&(v.$destroy(),v=null);p&&(d.leave(p).then(function(){u=null}),u=p,p=null)};c.$watch(f,function(f){var m=function(){!y(h)||h&&!c.$eval(h)||
b()},u=++s;f?(a(f,!0).then(function(a){if(u===s){var b=c.$new();n.template=a;a=q(b,function(a){C();d.enter(a,null,e).then(m)});v=b;p=a;v.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){u===s&&(C(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(C(),n.template=null)})}}}}],Pe=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){/SVG/.test(d[0].toString())?(d.empty(),a(Lc(e.template,X).childNodes)(b,function(a){d.append(a)},
{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],ze=La({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),Le=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=b.attr(d.$attr.ngList)||", ",f="false"!==d.ngTrim,g=f?U(e):e;c.$parsers.push(function(a){if(!q(a)){var b=[];a&&n(a.split(g),function(a){a&&b.push(f?U(a):a)});return b}});c.$formatters.push(function(a){return I(a)?a.join(e):u});c.$isEmpty=function(a){return!a||
!a.length}}}},mb="ng-valid",Jd="ng-invalid",Wa="ng-pristine",Jb="ng-dirty",Ld="ng-pending",lb=G("ngModel"),yg=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,b,d,c,e,f,g,h,k,l){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=u;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;
this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=u;this.$name=l(d.name||"",!1)(a);this.$$parentForm=Ib;var m=e(d.ngModel),r=m.assign,t=m,s=r,v=null,B,p=this;this.$$setOptions=function(a){if((p.$options=a)&&a.getterSetter){var b=e(d.ngModel+"()"),f=e(d.ngModel+"($$$p)");t=function(a){var c=m(a);z(c)&&(c=b(a));return c};s=function(a,b){z(m(a))?f(a,{$$$p:p.$modelValue}):r(a,p.$modelValue)}}else if(!m.assign)throw lb("nonassign",d.ngModel,ua(c));};this.$render=x;this.$isEmpty=
function(a){return q(a)||""===a||null===a||a!==a};var C=0;Gd({ctrl:this,$element:c,set:function(a,b){a[b]=!0},unset:function(a,b){delete a[b]},$animate:f});this.$setPristine=function(){p.$dirty=!1;p.$pristine=!0;f.removeClass(c,Jb);f.addClass(c,Wa)};this.$setDirty=function(){p.$dirty=!0;p.$pristine=!1;f.removeClass(c,Wa);f.addClass(c,Jb);p.$$parentForm.$setDirty()};this.$setUntouched=function(){p.$touched=!1;p.$untouched=!0;f.setClass(c,"ng-untouched","ng-touched")};this.$setTouched=function(){p.$touched=
!0;p.$untouched=!1;f.setClass(c,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){g.cancel(v);p.$viewValue=p.$$lastCommittedViewValue;p.$render()};this.$validate=function(){if(!Q(p.$modelValue)||!isNaN(p.$modelValue)){var a=p.$$rawModelValue,b=p.$valid,c=p.$modelValue,d=p.$options&&p.$options.allowInvalid;p.$$runValidators(a,p.$$lastCommittedViewValue,function(e){d||b===e||(p.$modelValue=e?a:u,p.$modelValue!==c&&p.$$writeModelToScope())})}};this.$$runValidators=function(a,b,c){function d(){var c=
!0;n(p.$validators,function(d,e){var g=d(a,b);c=c&&g;f(e,g)});return c?!0:(n(p.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;n(p.$asyncValidators,function(e,g){var h=e(a,b);if(!h||!z(h.then))throw lb("$asyncValidators",h);f(g,u);c.push(h.then(function(){f(g,!0)},function(a){d=!1;f(g,!1)}))});c.length?k.all(c).then(function(){g(d)},x):g(!0)}function f(a,b){h===C&&p.$setValidity(a,b)}function g(a){h===C&&c(a)}C++;var h=C;(function(){var a=p.$$parserName||"parse";if(q(B))f(a,
null);else return B||(n(p.$validators,function(a,b){f(b,null)}),n(p.$asyncValidators,function(a,b){f(b,null)})),f(a,B),B;return!0})()?d()?e():g(!1):g(!1)};this.$commitViewValue=function(){var a=p.$viewValue;g.cancel(v);if(p.$$lastCommittedViewValue!==a||""===a&&p.$$hasNativeValidators)p.$$lastCommittedViewValue=a,p.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var b=p.$$lastCommittedViewValue;if(B=q(b)?u:!0)for(var c=0;c<p.$parsers.length;c++)if(b=p.$parsers[c](b),
q(b)){B=!1;break}Q(p.$modelValue)&&isNaN(p.$modelValue)&&(p.$modelValue=t(a));var d=p.$modelValue,e=p.$options&&p.$options.allowInvalid;p.$$rawModelValue=b;e&&(p.$modelValue=b,p.$modelValue!==d&&p.$$writeModelToScope());p.$$runValidators(b,p.$$lastCommittedViewValue,function(a){e||(p.$modelValue=a?b:u,p.$modelValue!==d&&p.$$writeModelToScope())})};this.$$writeModelToScope=function(){s(a,p.$modelValue);n(p.$viewChangeListeners,function(a){try{a()}catch(c){b(c)}})};this.$setViewValue=function(a,b){p.$viewValue=
a;p.$options&&!p.$options.updateOnDefault||p.$$debounceViewValueCommit(b)};this.$$debounceViewValueCommit=function(b){var c=0,d=p.$options;d&&y(d.debounce)&&(d=d.debounce,Q(d)?c=d:Q(d[b])?c=d[b]:Q(d["default"])&&(c=d["default"]));g.cancel(v);c?v=g(function(){p.$commitViewValue()},c):h.$$phase?p.$commitViewValue():a.$apply(function(){p.$commitViewValue()})};a.$watch(function(){var b=t(a);if(b!==p.$modelValue&&(p.$modelValue===p.$modelValue||b===b)){p.$modelValue=p.$$rawModelValue=b;B=u;for(var c=p.$formatters,
d=c.length,e=b;d--;)e=c[d](e);p.$viewValue!==e&&(p.$viewValue=p.$$lastCommittedViewValue=e,p.$render(),p.$$runValidators(b,e,x))}return b})}],Ke=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:yg,priority:1,compile:function(b){b.addClass(Wa).addClass("ng-untouched").addClass(mb);return{pre:function(a,b,e,f){var g=f[0];b=f[1]||g.$$parentForm;g.$$setOptions(f[2]&&f[2].$options);b.$addControl(g);e.$observe("name",function(a){g.$name!==a&&g.$$parentForm.$$renameControl(g,
a)});a.$on("$destroy",function(){g.$$parentForm.$removeControl(g)})},post:function(b,c,e,f){var g=f[0];if(g.$options&&g.$options.updateOn)c.on(g.$options.updateOn,function(a){g.$$debounceViewValueCommit(a&&a.type)});c.on("blur",function(c){g.$touched||(a.$$phase?b.$evalAsync(g.$setTouched):b.$apply(g.$setTouched))})}}}}}],zg=/(\s+|^)default(\s+|$)/,Oe=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,b){var d=this;this.$options=bb(a.$eval(b.ngModelOptions));y(this.$options.updateOn)?
(this.$options.updateOnDefault=!1,this.$options.updateOn=U(this.$options.updateOn.replace(zg,function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},Ae=La({terminal:!0,priority:1E3}),Ag=G("ngOptions"),Bg=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,Ie=["$compile","$parse",function(a,
b){function d(a,c,d){function e(a,b,c,d,f){this.selectValue=a;this.viewValue=b;this.label=c;this.group=d;this.disabled=f}function l(a){var b;if(!q&&za(a))b=a;else{b=[];for(var c in a)a.hasOwnProperty(c)&&"$"!==c.charAt(0)&&b.push(c)}return b}var m=a.match(Bg);if(!m)throw Ag("iexp",a,ua(c));var n=m[5]||m[7],q=m[6];a=/ as /.test(m[0])&&m[1];var s=m[9];c=b(m[2]?m[1]:n);var v=a&&b(a)||c,u=s&&b(s),p=s?function(a,b){return u(d,b)}:function(a){return Ca(a)},C=function(a,b){return p(a,z(a,b))},w=b(m[2]||
m[1]),y=b(m[3]||""),B=b(m[4]||""),x=b(m[8]),D={},z=q?function(a,b){D[q]=b;D[n]=a;return D}:function(a){D[n]=a;return D};return{trackBy:s,getTrackByValue:C,getWatchables:b(x,function(a){var b=[];a=a||[];for(var c=l(a),e=c.length,f=0;f<e;f++){var g=a===c?f:c[f],k=z(a[g],g),g=p(a[g],k);b.push(g);if(m[2]||m[1])g=w(d,k),b.push(g);m[4]&&(k=B(d,k),b.push(k))}return b}),getOptions:function(){for(var a=[],b={},c=x(d)||[],f=l(c),g=f.length,m=0;m<g;m++){var n=c===f?m:f[m],r=z(c[n],n),q=v(d,r),n=p(q,r),t=w(d,
r),u=y(d,r),r=B(d,r),q=new e(n,q,t,u,r);a.push(q);b[n]=q}return{items:a,selectValueMap:b,getOptionFromViewValue:function(a){return b[C(a)]},getViewValueFromOption:function(a){return s?fa.copy(a.viewValue):a.viewValue}}}}}var c=X.createElement("option"),e=X.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","?ngModel"],link:{pre:function(a,b,c,d){d[0].registerOption=x},post:function(b,g,h,k){function l(a,b){a.element=b;b.disabled=a.disabled;a.label!==b.label&&(b.label=a.label,
b.textContent=a.label);a.value!==b.value&&(b.value=a.selectValue)}function m(a,b,c,d){b&&F(b.nodeName)===c?c=b:(c=d.cloneNode(!1),b?a.insertBefore(c,b):a.appendChild(c));return c}function r(a){for(var b;a;)b=a.nextSibling,Xb(a),a=b}function q(a){var b=p&&p[0],c=z&&z[0];if(b||c)for(;a&&(a===b||a===c||8===a.nodeType||""===a.value);)a=a.nextSibling;return a}function s(){var a=D&&u.readValue();D=E.getOptions();var b={},d=g[0].firstChild;x&&g.prepend(p);d=q(d);D.items.forEach(function(a){var f,h;a.group?
(f=b[a.group],f||(f=m(g[0],d,"optgroup",e),d=f.nextSibling,f.label=a.group,f=b[a.group]={groupElement:f,currentOptionElement:f.firstChild}),h=m(f.groupElement,f.currentOptionElement,"option",c),l(a,h),f.currentOptionElement=h.nextSibling):(h=m(g[0],d,"option",c),l(a,h),d=h.nextSibling)});Object.keys(b).forEach(function(a){r(b[a].currentOptionElement)});r(d);v.$render();if(!v.$isEmpty(a)){var f=u.readValue();(E.trackBy?ma(a,f):a===f)||(v.$setViewValue(f),v.$render())}}var v=k[1];if(v){var u=k[0];k=
h.multiple;for(var p,C=0,w=g.children(),y=w.length;C<y;C++)if(""===w[C].value){p=w.eq(C);break}var x=!!p,z=B(c.cloneNode(!1));z.val("?");var D,E=d(h.ngOptions,g,b);k?(v.$isEmpty=function(a){return!a||0===a.length},u.writeValue=function(a){D.items.forEach(function(a){a.element.selected=!1});a&&a.forEach(function(a){(a=D.getOptionFromViewValue(a))&&!a.disabled&&(a.element.selected=!0)})},u.readValue=function(){var a=g.val()||[],b=[];n(a,function(a){(a=D.selectValueMap[a])&&!a.disabled&&b.push(D.getViewValueFromOption(a))});
return b},E.trackBy&&b.$watchCollection(function(){if(I(v.$viewValue))return v.$viewValue.map(function(a){return E.getTrackByValue(a)})},function(){v.$render()})):(u.writeValue=function(a){var b=D.getOptionFromViewValue(a);b&&!b.disabled?g[0].value!==b.selectValue&&(z.remove(),x||p.remove(),g[0].value=b.selectValue,b.element.selected=!0,b.element.setAttribute("selected","selected")):null===a||x?(z.remove(),x||g.prepend(p),g.val(""),p.prop("selected",!0),p.attr("selected",!0)):(x||p.remove(),g.prepend(z),
g.val("?"),z.prop("selected",!0),z.attr("selected",!0))},u.readValue=function(){var a=D.selectValueMap[g.val()];return a&&!a.disabled?(x||p.remove(),z.remove(),D.getViewValueFromOption(a)):null},E.trackBy&&b.$watch(function(){return E.getTrackByValue(v.$viewValue)},function(){v.$render()}));x?(p.remove(),a(p)(b),p.removeClass("ng-scope")):p=B(c.cloneNode(!1));s();b.$watchCollection(E.getWatchables,s)}}}}}],Be=["$locale","$interpolate","$log",function(a,b,d){var c=/{}/g,e=/^when(Minus)?(.+)$/;return{link:function(f,
g,h){function k(a){g.text(a||"")}var l=h.count,m=h.$attr.when&&g.attr(h.$attr.when),r=h.offset||0,s=f.$eval(m)||{},u={},v=b.startSymbol(),y=b.endSymbol(),p=v+l+"-"+r+y,C=fa.noop,w;n(h,function(a,b){var c=e.exec(b);c&&(c=(c[1]?"-":"")+F(c[2]),s[c]=g.attr(h.$attr[b]))});n(s,function(a,d){u[d]=b(a.replace(c,p))});f.$watch(l,function(b){var c=parseFloat(b),e=isNaN(c);e||c in s||(c=a.pluralCat(c-r));c===w||e&&Q(w)&&isNaN(w)||(C(),e=u[c],q(e)?(null!=b&&d.debug("ngPluralize: no rule defined for '"+c+"' in "+
m),C=x,k()):C=f.$watch(e,k),w=c)})}}}],Ce=["$parse","$animate",function(a,b){var d=G("ngRepeat"),c=function(a,b,c,d,k,l,m){a[c]=d;k&&(a[k]=l);a.$index=b;a.$first=0===b;a.$last=b===m-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(b&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(e,f){var g=f.ngRepeat,h=X.createComment(" end ngRepeat: "+g+" "),k=g.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if(!k)throw d("iexp",g);var l=k[1],m=k[2],r=k[3],q=k[4],k=l.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);if(!k)throw d("iidexp",l);var s=k[3]||k[1],v=k[2];if(r&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(r)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(r)))throw d("badident",r);var x,p,y,w,z={$id:Ca};q?x=a(q):(y=function(a,b){return Ca(b)},w=function(a){return a});return function(a,e,f,k,l){x&&(p=function(b,c,d){v&&(z[v]=b);z[s]=c;z.$index=
d;return x(a,z)});var q=$();a.$watchCollection(m,function(f){var k,m,t=e[0],x,z=$(),D,E,H,F,I,G,J;r&&(a[r]=f);if(za(f))I=f,m=p||y;else for(J in m=p||w,I=[],f)qa.call(f,J)&&"$"!==J.charAt(0)&&I.push(J);D=I.length;J=Array(D);for(k=0;k<D;k++)if(E=f===I?k:I[k],H=f[E],F=m(E,H,k),q[F])G=q[F],delete q[F],z[F]=G,J[k]=G;else{if(z[F])throw n(J,function(a){a&&a.scope&&(q[a.id]=a)}),d("dupes",g,F,H);J[k]={id:F,scope:u,clone:u};z[F]=!0}for(x in q){G=q[x];F=rb(G.clone);b.leave(F);if(F[0].parentNode)for(k=0,m=F.length;k<
m;k++)F[k].$$NG_REMOVED=!0;G.scope.$destroy()}for(k=0;k<D;k++)if(E=f===I?k:I[k],H=f[E],G=J[k],G.scope){x=t;do x=x.nextSibling;while(x&&x.$$NG_REMOVED);G.clone[0]!=x&&b.move(rb(G.clone),null,B(t));t=G.clone[G.clone.length-1];c(G.scope,k,s,H,v,E,D)}else l(function(a,d){G.scope=d;var e=h.cloneNode(!1);a[a.length++]=e;b.enter(a,null,B(t));t=e;G.clone=a;z[G.id]=G;c(G.scope,k,s,H,v,E,D)});q=z})}}}}],De=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngShow,function(b){a[b?
"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],we=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngHide,function(b){a[b?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],Ee=La(function(a,b,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&n(d,function(a,c){b.css(c,"")});a&&b.css(a)},!0)}),Fe=["$animate",function(a){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(b,
d,c,e){var f=[],g=[],h=[],k=[],l=function(a,b){return function(){a.splice(b,1)}};b.$watch(c.ngSwitch||c.on,function(b){var c,d;c=0;for(d=h.length;c<d;++c)a.cancel(h[c]);c=h.length=0;for(d=k.length;c<d;++c){var q=rb(g[c].clone);k[c].$destroy();(h[c]=a.leave(q)).then(l(h,c))}g.length=0;k.length=0;(f=e.cases["!"+b]||e.cases["?"])&&n(f,function(b){b.transclude(function(c,d){k.push(d);var e=b.element;c[c.length++]=X.createComment(" end ngSwitchWhen: ");g.push({clone:c});a.enter(c,e.parent(),e)})})})}}}],
Ge=La({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["!"+d.ngSwitchWhen]=c.cases["!"+d.ngSwitchWhen]||[];c.cases["!"+d.ngSwitchWhen].push({transclude:e,element:b})}}),He=La({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["?"]=c.cases["?"]||[];c.cases["?"].push({transclude:e,element:b})}}),Je=La({restrict:"EAC",link:function(a,b,d,c,e){if(!e)throw G("ngTransclude")("orphan",ua(b));e(function(a){b.empty();
b.append(a)})}}),je=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(b,d){"text/ng-template"==d.type&&a.put(d.id,b[0].text)}}}],Cg={$setViewValue:x,$render:x},Dg=["$element","$scope","$attrs",function(a,b,d){var c=this,e=new Sa;c.ngModelCtrl=Cg;c.unknownOption=B(X.createElement("option"));c.renderUnknownOption=function(b){b="? "+Ca(b)+" ?";c.unknownOption.val(b);a.prepend(c.unknownOption);a.val(b)};b.$on("$destroy",function(){c.renderUnknownOption=x});c.removeUnknownOption=
function(){c.unknownOption.parent()&&c.unknownOption.remove()};c.readValue=function(){c.removeUnknownOption();return a.val()};c.writeValue=function(b){c.hasOption(b)?(c.removeUnknownOption(),a.val(b),""===b&&c.emptyOption.prop("selected",!0)):null==b&&c.emptyOption?(c.removeUnknownOption(),a.val("")):c.renderUnknownOption(b)};c.addOption=function(a,b){Ra(a,'"option value"');""===a&&(c.emptyOption=b);var d=e.get(a)||0;e.put(a,d+1);c.ngModelCtrl.$render();b[0].hasAttribute("selected")&&(b[0].selected=
!0)};c.removeOption=function(a){var b=e.get(a);b&&(1===b?(e.remove(a),""===a&&(c.emptyOption=u)):e.put(a,b-1))};c.hasOption=function(a){return!!e.get(a)};c.registerOption=function(a,b,d,e,l){if(e){var m;d.$observe("value",function(a){y(m)&&c.removeOption(m);m=a;c.addOption(a,b)})}else l?a.$watch(l,function(a,e){d.$set("value",a);e!==a&&c.removeOption(e);c.addOption(a,b)}):c.addOption(d.value,b);b.on("$destroy",function(){c.removeOption(d.value);c.ngModelCtrl.$render()})}}],ke=function(){return{restrict:"E",
require:["select","?ngModel"],controller:Dg,priority:1,link:{pre:function(a,b,d,c){var e=c[1];if(e){var f=c[0];f.ngModelCtrl=e;e.$render=function(){f.writeValue(e.$viewValue)};b.on("change",function(){a.$apply(function(){e.$setViewValue(f.readValue())})});if(d.multiple){f.readValue=function(){var a=[];n(b.find("option"),function(b){b.selected&&a.push(b.value)});return a};f.writeValue=function(a){var c=new Sa(a);n(b.find("option"),function(a){a.selected=y(c.get(a.value))})};var g,h=NaN;a.$watch(function(){h!==
e.$viewValue||ma(g,e.$viewValue)||(g=ia(e.$viewValue),e.$render());h=e.$viewValue});e.$isEmpty=function(a){return!a||0===a.length}}}}}}},me=["$interpolate",function(a){return{restrict:"E",priority:100,compile:function(b,d){if(y(d.value))var c=a(d.value,!0);else{var e=a(b.text(),!0);e||d.$set("value",b.text())}return function(a,b,d){var k=b.parent();(k=k.data("$selectController")||k.parent().data("$selectController"))&&k.registerOption(a,b,d,c,e)}}}}],le=na({restrict:"E",terminal:!1}),Fc=function(){return{restrict:"A",
require:"?ngModel",link:function(a,b,d,c){c&&(d.required=!0,c.$validators.required=function(a,b){return!d.required||!c.$isEmpty(b)},d.$observe("required",function(){c.$validate()}))}}},Ec=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e,f=d.ngPattern||d.pattern;d.$observe("pattern",function(a){E(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)throw G("ngPattern")("noregexp",f,a,ua(b));e=a||u;c.$validate()});c.$validators.pattern=function(a,b){return c.$isEmpty(b)||
q(e)||e.test(b)}}}}},Hc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=-1;d.$observe("maxlength",function(a){a=ea(a);e=isNaN(a)?-1:a;c.$validate()});c.$validators.maxlength=function(a,b){return 0>e||c.$isEmpty(b)||b.length<=e}}}}},Gc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=ea(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};S.angular.bootstrap?
console.log("WARNING: Tried to load angular more than once."):(ce(),ee(fa),fa.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),
SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",
negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",pluralCat:function(a,c){var e=a|0,f=c;u===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),B(X).ready(function(){Zd(X,yc)}))})(window,document);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
//# sourceMappingURL=angular.min.js.map

1
admin/static/js/angular.min.js vendored Symbolic link
View file

@ -0,0 +1 @@
../../../frontend/static/js/angular.min.js

View file

@ -1,4 +1,4 @@
angular.module("FICApp", ["ngRoute", "ngResource"])
angular.module("FICApp", ["ngRoute", "ngResource", "ngSanitize"])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when("/themes", {
@ -9,7 +9,19 @@ angular.module("FICApp", ["ngRoute", "ngResource"])
controller: "ThemeController",
templateUrl: "views/theme.html"
})
.when("/themes/:themeId/:exerciceId", {
.when("/themes/:themeId/exercices/:exerciceId", {
controller: "ExerciceController",
templateUrl: "views/exercice.html"
})
.when("/settings", {
controller: "SettingsController",
templateUrl: "views/settings.html"
})
.when("/exercices", {
controller: "AllExercicesListController",
templateUrl: "views/exercice-list.html"
})
.when("/exercices/:exerciceId", {
controller: "ExerciceController",
templateUrl: "views/exercice.html"
})
@ -17,14 +29,34 @@ angular.module("FICApp", ["ngRoute", "ngResource"])
controller: "TeamsListController",
templateUrl: "views/team-list.html"
})
.when("/teams/:teamId", {
controller: "TeamController",
templateUrl: "views/team.html"
})
.when("/teams/new", {
controller: "TeamNewController",
templateUrl: "views/team-new.html"
})
.when("/teams/print", {
controller: "TeamsListController",
templateUrl: "views/team-print.html"
})
.when("/teams/:teamId", {
controller: "TeamController",
templateUrl: "views/team-edit.html"
})
.when("/teams/:teamId/stats", {
controller: "TeamController",
templateUrl: "views/team-stats.html"
})
.when("/public", {
controller: "PublicController",
templateUrl: "views/public.html"
})
.when("/events", {
controller: "EventsListController",
templateUrl: "views/event-list.html"
})
.when("/events/:eventId", {
controller: "EventController",
templateUrl: "views/event.html"
})
.when("/", {
templateUrl: "views/home.html"
});
@ -35,19 +67,36 @@ angular.module("FICApp")
.factory("Version", function($resource) {
return $resource("/api/version")
})
.factory("Event", function($resource) {
return $resource("/api/events/:eventId", { eventId: '@id' }, {
'update': {method: 'PUT'},
})
})
.factory("Settings", function($resource) {
return $resource("/api/settings.json", null, {
'update': {method: 'PUT'},
})
})
.factory("Scene", function($resource) {
return $resource("/api/public.json", null, {
'update': {method: 'PUT'},
})
})
.factory("Team", function($resource) {
return $resource("/api/teams/:teamId", { teamId: '@id' }, {
'save': {method: 'PATCH'},
'update': {method: 'PUT'},
})
})
.factory("TeamMember", function($resource) {
return $resource("/api/teams/:teamId/members", { teamId: '@id' })
return $resource("/api/teams/:teamId/members", { teamId: '@id' }, {
'save': {method: 'PUT'},
})
})
.factory("TeamMy", function($resource) {
return $resource("/api/teams/:teamId/my.json", { teamId: '@id' })
})
.factory("Teams", function($resource) {
return $resource("/api/teams/teams.json")
return $resource("/api/teams.json")
})
.factory("TeamStats", function($resource) {
return $resource("/api/teams/:teamId/stats.json", { teamId: '@id' })
@ -56,17 +105,39 @@ angular.module("FICApp")
return $resource("/api/teams/:teamId/tries", { teamId: '@id' })
})
.factory("Theme", function($resource) {
return $resource("/api/themes/:themeId", null, {
'save': {method: 'PATCH'},
})
return $resource("/api/themes/:themeId", { themeId: '@id' }, {
update: {method: 'PUT'}
});
})
.factory("Themes", function($resource) {
return $resource("/api/themes/themes.json", null, {
return $resource("/api/themes.json", null, {
'get': {method: 'GET'},
})
})
.factory("ThemedExercice", function($resource) {
return $resource("/api/themes/:themeId/exercices/:exerciceId", { exerciceId: '@id' }, {
update: {method: 'PUT'}
})
})
.factory("Exercice", function($resource) {
return $resource("/api/exercices/:exerciceId")
return $resource("/api/exercices/:exerciceId", { exerciceId: '@id' }, {
update: {method: 'PUT'}
})
})
.factory("ExerciceFile", function($resource) {
return $resource("/api/exercices/:exerciceId/files/:fileId", { exerciceId: '@idExercice', fileId: '@id' }, {
update: {method: 'PUT'}
})
})
.factory("ExerciceHint", function($resource) {
return $resource("/api/exercices/:exerciceId/hints/:hintId", { exerciceId: '@idExercice', hintId: '@id' }, {
update: {method: 'PUT'}
})
})
.factory("ExerciceKey", function($resource) {
return $resource("/api/exercices/:exerciceId/keys/:keyId", { exerciceId: '@idExercice', keyId: '@id' }, {
update: {method: 'PUT'}
})
});
String.prototype.capitalize = function() {
@ -85,6 +156,51 @@ angular.module("FICApp")
return input.capitalize();
}
})
.filter("toColor", function() {
return function(input) {
num >>>= 0;
var b = num & 0xFF,
g = (num & 0xFF00) >>> 8,
r = (num & 0xFF0000) >>> 16,
a = ( (num & 0xFF000000) >>> 24 ) / 255 ;
return "#" + r.toString(16) + g.toString(16) + b.toString(16);
}
})
.filter("size", function() {
var units = [
"o",
"kio",
"Mio",
"Gio",
"Tio",
"Pio",
"Eio",
"Zio",
"Yio",
]
return function(input) {
var res = input;
var unit = 0;
while (res > 1024) {
unit += 1;
res = res / 1024;
}
return (Math.round(res * 100) / 100) + " " + units[unit];
}
})
.filter("cksum", function() {
return function(input) {
if (input == undefined)
return input;
var raw = atob(input).toString(16);
var hex = '';
for (var i = 0; i < raw.length; i++ ) {
var _hex = raw.charCodeAt(i).toString(16)
hex += (_hex.length == 2 ? _hex : '0' + _hex);
}
return hex
}
})
.filter("time", function() {
return function(input) {
if (input == undefined) {
@ -97,10 +213,194 @@ angular.module("FICApp")
}
})
.directive('color', function() {
return {
require: 'ngModel',
link: function(scope, ele, attr, ctrl){
ctrl.$formatters.unshift(function(num){
num >>>= 0;
var b = num & 0xFF,
g = (num & 0xFF00) >>> 8,
r = (num & 0xFF0000) >>> 16,
a = ( (num & 0xFF000000) >>> 24 ) / 255 ;
return "#" + r.toString(16) + g.toString(16) + b.toString(16);
});
ctrl.$parsers.unshift(function(viewValue){
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(viewValue);
return result ? (
parseInt(result[1], 16) * 256 * 256 +
parseInt(result[2], 16) * 256 +
parseInt(result[3], 16)
) : 0;
});
}
};
})
.directive('integer', function() {
return {
require: 'ngModel',
link: function(scope, ele, attr, ctrl){
ctrl.$parsers.unshift(function(viewValue){
return parseInt(viewValue, 10);
});
}
};
})
.directive('float', function() {
return {
require: 'ngModel',
link: function(scope, ele, attr, ctrl){
ctrl.$parsers.unshift(function(viewValue){
return parseFloat(viewValue, 10);
});
}
};
})
.controller("VersionController", function($scope, Version) {
$scope.v = Version.get();
})
.controller("SettingsController", function($scope, Settings, $location, $http) {
$scope.config = Settings.get();
$scope.duration = 240;
$scope.saveSettings = function() {
this.config.$update(function() {
$location.url("/");
});
}
$scope.regenerate = function() {
this.config.generation = (new Date()).toISOString();
$scope.saveSettings();
}
$scope.launchChallenge = function() {
var ts = Date.now() - Date.now() % 60000;
var d = new Date(ts + 120000);
this.config.start = d.toISOString();
var f = new Date(ts + 120000 + this.duration * 60000);
this.config.end = f.toISOString();
}
$scope.reset = function(type) {
if (confirm("Êtes-vous sûr ?")) {
$http.post("/api/reset", {"type": type}).success(function(time) {
$location.url("/");
});
}
};
})
.controller("PublicController", function($scope, Scene, Theme, Teams, Exercice) {
$scope.scenes = Scene.query();
$scope.themes = Theme.query();
$scope.teams = Teams.get();
$scope.types = {
"welcome": "Messages de bienvenue",
"message": "Message",
"panel": "Boîte",
"exercice": "Exercice",
"table": "Tableau",
"rank": "Classement",
};
$scope.welcome_types = {
"init": "Accueil des équipes",
"public": "Accueil du public",
"countdown": "Compte à rebours lancement",
};
$scope.panel_types = {
"panel-default": "Default",
"panel-info": "Info",
"panel-success": "Success",
"panel-warning": "Warning",
"panel-danger": "Danger",
};
$scope.rank_types = {
"general": "Classement général",
};
$scope.table_types = {
"levels": "Niveaux d'exercices",
"teams": "Équipes",
};
$scope.exercices = Exercice.query();
$scope.clearScene = function() {
Scene.delete(function() {
$scope.scenes = [];
});
};
$scope.saveScenes = function() {
Scene.update($scope.scenes);
};
$scope.addScene = function() {
$scope.scenes.push({params: {}});
};
$scope.delScene = function(s) {
angular.forEach($scope.scenes, function(scene, k) {
if (scene == s)
$scope.scenes.splice(k, 1);
});
};
$scope.upScene = function(s) {
angular.forEach($scope.scenes, function(scene, k) {
if (scene == s && k > 0) {
$scope.scenes.splice(k, 1);
$scope.scenes.splice(k - 1, 0, scene);
}
});
};
$scope.downScene = function(s) {
var move = true;
angular.forEach($scope.scenes, function(scene, k) {
if (move && scene == s) {
$scope.scenes.splice(k, 1);
$scope.scenes.splice(k + 1, 0, scene);
move = false;
}
});
};
})
.controller("EventsListController", function($scope, Event, $location) {
$scope.events = Event.query();
$scope.fields = ["id", "kind", "txt", "time"];
$scope.clearEvents = function(id) {
Event.delete(function() {
$scope.events = [];
});
};
$scope.show = function(id) {
$location.url("/events/" + id);
};
})
.controller("EventController", function($scope, Event, $routeParams, $location) {
$scope.event = Event.get({ eventId: $routeParams.eventId });
$scope.fields = ["kind", "txt", "time"];
$scope.kinds = {
"alert-info": "Info",
"alert-warning": "Warning",
"alert-success": "Success",
"alert-danger": "Danger",
};
$scope.saveEvent = function() {
if (this.event.id) {
this.event.$update();
} else {
this.event.$save(function() {
$location.url("/events/" + $scope.event.id);
});
}
}
$scope.deleteEvent = function() {
this.event.$remove(function() { $location.url("/events/");});
}
})
.controller("ThemesListController", function($scope, Theme, $location) {
$scope.themes = Theme.query();
$scope.fields = ["id", "name"];
@ -109,29 +409,111 @@ angular.module("FICApp")
$location.url("/themes/" + id);
};
})
.controller("ThemeController", function($scope, Theme, $routeParams) {
.controller("ThemeController", function($scope, Theme, $routeParams, $location) {
$scope.theme = Theme.get({ themeId: $routeParams.themeId });
$scope.fields = ["name"];
$scope.fields = ["name", "authors"];
$scope.saveTheme = function() {
this.theme.$save({themeId: this.theme.themeId});
if (this.theme.id) {
this.theme.$update();
} else {
this.theme.$save(function() {
$location.url("/themes/" + $scope.theme.id);
});
}
}
$scope.deleteTheme = function() {
this.theme.$remove(function() { $location.url("/themes/");});
}
})
.controller("ExercicesListController", function($scope, Exercice, $routeParams, $location) {
$scope.exercices = Exercice.query({ themeId: $routeParams.themeId });
$scope.fields = ["id", "title", "statement", "videoURI"];
.controller("AllExercicesListController", function($scope, Exercice, $routeParams, $location) {
$scope.exercices = Exercice.query();
$scope.fields = ["title", "statement", "videoURI"];
$scope.show = function(id) {
$location.url("/themes/" + $routeParams.themeId + "/" + id);
$location.url("/exercices/" + id);
};
})
.controller("ExerciceController", function($scope, Theme, $routeParams) {
$scope.exercice = Exercice.get({ themeId: $routeParams.themeId });
$scope.fields = ["name", "statement", "hint", "videoURI"];
.controller("ExercicesListController", function($scope, ThemedExercice, $routeParams, $location) {
$scope.exercices = ThemedExercice.query({ themeId: $routeParams.themeId });
$scope.fields = ["title", "statement", "videoURI"];
$scope.saveTheme = function() {
this.exercice.$save({ themeId: this.exercice.themeId, exerciceId: this.exercice.exerciceId});
$scope.show = function(id) {
$location.url("/themes/" + $routeParams.themeId + "/exercices/" + id);
};
})
.controller("ExerciceController", function($scope, Exercice, ThemedExercice, $routeParams, $location) {
if ($routeParams.themeId && $routeParams.exerciceId == "new") {
$scope.exercice = new ThemedExercice();
} else {
$scope.exercice = Exercice.get({ exerciceId: $routeParams.exerciceId });
}
$scope.exercices = Exercice.query();
$scope.fields = ["title", "statement", "depend", "gain", "coefficient", "videoURI"];
$scope.saveExercice = function() {
if (this.exercice.id) {
this.exercice.$update();
} else if ($routeParams.themeId) {
this.exercice.$save({ themeId: $routeParams.themeId }, function() {
$location.url("/themes/" + $scope.exercice.idTheme + "/exercices/" + $scope.exercice.id);
});
}
}
})
.controller("ExerciceFilesController", function($scope, ExerciceFile, $routeParams, $location) {
$scope.files = ExerciceFile.query({ exerciceId: $routeParams.exerciceId });
$scope.deleteFile = function() {
this.file.$delete(function() {
$scope.files.splice($scope.files.indexOf(this.file), 1);
});
return false;
}
$scope.saveFile = function() {
this.file.$update();
}
})
.controller("ExerciceHintsController", function($scope, ExerciceHint, $routeParams) {
$scope.hints = ExerciceHint.query({ exerciceId: $routeParams.exerciceId });
$scope.addHint = function() {
$scope.hints.push(new ExerciceHint());
}
$scope.deleteHint = function() {
this.hint.$delete(function() {
$scope.hints.splice($scope.hints.indexOf(this.hint), 1);
});
}
$scope.saveHint = function() {
if (this.hint.id) {
this.hint.$update();
} else {
this.hint.$save({ exerciceId: $routeParams.exerciceId });
}
}
})
.controller("ExerciceKeysController", function($scope, ExerciceKey, $routeParams) {
$scope.keys = ExerciceKey.query({ exerciceId: $routeParams.exerciceId });
$scope.addKey = function() {
$scope.keys.push(new ExerciceKey());
}
$scope.deleteKey = function() {
this.key.$delete(function() {
$scope.keys.splice($scope.keys.indexOf(this.key), 1);
});
}
$scope.saveKey = function() {
if (this.key.id) {
this.key.$update();
} else {
this.key.$save({ exerciceId: $routeParams.exerciceId });
}
}
})
@ -143,15 +525,93 @@ angular.module("FICApp")
$location.url("/teams/" + id);
};
})
.controller("TeamController", function($scope, Team, TeamMember, $routeParams) {
.controller("TeamMembersController", function($scope, TeamMember) {
$scope.fields = ["firstname", "lastname", "nickname", "company"];
if ($scope.team != null) {
$scope.members = TeamMember.query({ teamId: $scope.team.id });
$scope.newMember = function() {
$scope.members.push(new TeamMember());
}
$scope.saveTeamMembers = function() {
if (this.team.id) {
TeamMember.save({ teamId: this.team.id }, $scope.members);
}
}
$scope.removeMember = function(member) {
angular.forEach($scope.members, function(m, k) {
if (member == m)
$scope.members.splice(k, 1);
});
}
}
})
.controller("TeamController", function($scope, $location, Team, TeamMember, $routeParams, $http) {
$scope.team = Team.get({ teamId: $routeParams.teamId });
$scope.members = TeamMember.query({ teamId: $routeParams.teamId });
$scope.fields = ["name", "color"];
$scope.hasCertificate = false;
$http({
url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate.p12",
method: "HEAD",
transformResponse: null
}).then(function(response) {
$scope.hasCertificate = true;
}, function(response) {
$scope.hasCertificate = false;
});
$scope.generateCertificate = function() {
$http({
url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate/generate",
method: "GET",
transformResponse: null
}).then(function(response) {
$scope.hasCertificate = true;
}, function(response) {
console.log(response.data);
});
}
$scope.revokeCertificate = function() {
if (!confirm("Are you sure you want to revoke this certificate?"))
return false;
$http({
url: "/api/teams/" + Math.floor($routeParams.teamId) + "/certificate.p12",
method: "DELETE",
transformResponse: null
}).then(function(response) {
$scope.hasCertificate = false;
}, function(response) {
console.log(response.data);
});
}
$scope.saveTeam = function() {
if (this.team.id) {
this.team.$update();
} else {
this.team.$save(function() {
$location.url("/teams/" + $scope.team.id);
});
}
}
$scope.deleteTeam = function() {
this.team.$remove(function() { $location.url("/teams/");});
}
$scope.showStats = function() {
$location.url("/teams/" + $scope.team.id + "/stats");
}
})
.controller("TeamStatsController", function($scope, TeamStats, $routeParams) {
$scope.teamstats = TeamStats.get({ teamId: $routeParams.teamId });
$scope.teamstats.$promise.then(function(res) {
solvedByLevelPie("#pieLevels", res.levels);
solvedByThemesPie("#pieThemes", res.themes);
var themes = [];
angular.forEach(res.themes, function(theme, tid) {
themes.push(theme);
})
solvedByThemesPie("#pieThemes", themes);
});
})
.controller("TeamExercicesController", function($scope, Teams, Themes, TeamMy, Exercice, $routeParams) {
@ -172,7 +632,7 @@ angular.module("FICApp")
$scope.my.$promise.then(function(res){
$scope.solved_exercices = 0;
angular.forEach(res.exercices, function(exercice, eid) {
if (exercice.solved) {
if (exercice.solved_rank) {
$scope.solved_exercices += 1;
}
}, 0);
@ -188,10 +648,10 @@ angular.module("FICApp")
.controller("PresenceController", function($scope, TeamPresence, $routeParams) {
$scope.presence = TeamPresence.query({ teamId: $routeParams.teamId });
$scope.presence.$promise.then(function(res) {
presenceCal("#presenceCal", res);
presenceCal($scope, "#presenceCal", res);
});
})
.controller("CountdownController", function($scope, $http, $timeout) {
.controller("CountdownController", function($scope, $rootScope, $http, $timeout) {
$scope.time = {};
function updTime() {
$timeout.cancel($scope.cbm);
@ -204,7 +664,10 @@ angular.module("FICApp")
$scope.refresh(true);
}
if (time.st > 0 && time.st <= srv_cur) {
$scope.startIn = 0;
remain = time.st + time.du - srv_cur;
} else if (time.st > 0) {
$scope.startIn = Math.floor(time.st - srv_cur);
}
if (remain < 0) {
remain = 0;
@ -223,6 +686,7 @@ angular.module("FICApp")
$scope.time.hours = Math.floor(remain / 3600);
$scope.time.minutes = Math.floor((remain % 3600) / 60);
$scope.time.seconds = Math.floor(remain % 60);
$rootScope.time = $scope.time;
}
}
@ -375,7 +839,7 @@ function solvedByThemesPie(location, data) {
.text(function(d) { return d.data.tip + ": " + d.data.tries; });
}
function presenceCal(location, data) {
function presenceCal(scope, location, data) {
var width = d3.select(location).node().getBoundingClientRect().width,
height = 80,
cellSize = 17; // cell size
@ -388,7 +852,7 @@ function presenceCal(location, data) {
.range(d3.range(8).map(function(d) { return "q" + d + "-8"; }));
var svg = d3.select(location).selectAll("svg")
.data(d3.range(26, 29))
.data(d3.range(scope.time.start, scope.time.start + (scope.time.start % 86400000 + scope.time.duration), 86400000).map(function(t) { return new Date(t); }))
.enter().append("svg")
.attr("width", width)
.attr("height", height)
@ -399,14 +863,15 @@ function presenceCal(location, data) {
svg.append("text")
.attr("transform", "translate(-6," + cellSize * 2.6 + ")rotate(-90)")
.style("text-anchor", "middle")
.text(function(d) { return d + "-02"; });
.text(function(d) { return d.getDate() + "-" + (d.getMonth() + 1); });
var rect = svg.selectAll(".quarter")
.data(function(d) { return d3.time.minutes(new Date(2016, 1, d, 0), new Date(2016, 1, d, 24), 15); })
.data(function(d) { return d3.time.minutes(new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0), new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23), 15); })
.enter().append("rect")
.attr("width", cellSize)
.attr("height", cellSize)
.attr("class", function(d) { return color(data.reduce(function(prev, cur){
.attr("transform", function(d) { return "translate(" + (d.getHours() * cellSize) + "," + (d.getMinutes() / 15 * cellSize) + ")"; })
.attr("class", function(d) { if (d >= scope.time.start && d < scope.time.start + scope.time.duration) return color(data.reduce(function(prev, cur){
cur = new Date(cur).getTime();
dv = d.getTime();
return prev + ((dv <= cur && cur < dv+15*60000)?1:0);

File diff suppressed because one or more lines are too long

1
admin/static/js/bootstrap.min.js vendored Symbolic link
View file

@ -0,0 +1 @@
../../../frontend/static/js/bootstrap.min.js

1
admin/static/js/d3.v3.min.js vendored Symbolic link
View file

@ -0,0 +1 @@
../../../frontend/static/js/d3.v3.min.js

1
admin/static/js/i18n Symbolic link
View file

@ -0,0 +1 @@
../../../frontend/static/js/i18n/

File diff suppressed because one or more lines are too long

1
admin/static/js/jquery.min.js vendored Symbolic link
View file

@ -0,0 +1 @@
../../../frontend/static/js/jquery.min.js

View file

@ -0,0 +1,19 @@
<h2>&Eacute;vénements<a ng-click="clearEvents()" class="pull-right btn btn-danger"><span class="glyphicon glyphicon-remove-sign" aria-hidden="true"></span> Vider la liste</a><a ng-click="show('new')" class="pull-right btn btn-primary" style="margin-right: 10px"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un événement</a></h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query"></p>
<table class="table table-hover table-bordered">
<thead>
<tr>
<th ng-repeat="field in fields">
{{ field }}
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in events | filter: query" ng-click="show(event.id)">
<td ng-repeat="field in fields">
{{ event[field] }}
</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,20 @@
<h2>&Eacute;vénement</h2>
<form ng-submit="saveEvent()" class="form-horizontal">
<div class="form-group" ng-repeat="field in fields">
<label for="{{ field }}" class="col-sm-1 control-label">{{ field | capitalize }}</label>
<div class="col-sm-11">
<input type="text" class="form-control" id="{{ field }}" ng-model="event[field]" ng-show="field != 'kind' && field != 'time'">
<input type="datetime" class="form-control" id="{{ field }}" ng-model="event[field]" ng-show="field == 'time' && event.id">
<select class="form-control" id="{{ field }}" ng-model="event[field]" ng-options="k as v for (k, v) in kinds" ng-show="field == 'kind'">
</select>
</div>
</div>
<div class="text-right" ng-show="event.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button>
<a class="btn btn-danger" ng-click="deleteEvent()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</a>
</div>
<div class="text-right" ng-show="!event.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter l'événement</button>
</div>
</form>

View file

@ -0,0 +1,21 @@
<h2>Exercices</h2>
<div>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query"></p>
<table class="table table-hover table-bordered">
<thead>
<tr>
<th ng-repeat="field in fields">
{{ field }}
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="exercice in exercices | filter: query" ng-click="show(exercice.id)">
<td ng-repeat="field in fields">
{{ exercice[field] }}
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,118 @@
<h2>{{exercice.title}}</h2>
<form class="form-horizontal" ng-submit="saveExercice()">
<div class="form-group" ng-repeat="field in fields">
<label for="{{ field }}" class="col-xs-1 control-label">{{ field | capitalize }}</label>
<div class="col-xs-11">
<input type="text" class="form-control" id="{{ field }}" ng-model="exercice[field]" ng-show="field != 'statement' && field != 'depend' && field != 'gain' && field != 'coefficient'">
<input type="text" class="form-control" id="{{ field }}" ng-model="exercice[field]" ng-show="field == 'gain'" integer>
<input type="text" class="form-control" id="{{ field }}" ng-model="exercice[field]" ng-show="field == 'coefficient'" float>
<textarea class="form-control" id="{{field}}" ng-model="exercice[field]" ng-show="field == 'statement'"></textarea>
<select class="form-control" id="{{field}}" ng-model="exercice[field]" ng-options="ex.id as ex.title for ex in exercices" ng-show="field == 'depend'">
<option value="">Aucune</option>
</select>
</div>
</div>
<div class="text-right" ng-show="exercice.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button>
<a class="btn btn-danger" ng-click="deleteExercice()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</a>
</div>
<div class="text-right" ng-show="!exercice.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create exercice</button>
</div>
</form>
<hr>
<div class="row" ng-show="exercice.id">
<div class="col-md-4" ng-controller="ExerciceHintsController">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Indices<a ng-click="addHint()" class="pull-right btn btn-xs btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a></h3>
</div>
<div class="list-group">
<form ng-submit="saveHint()" class="list-group-item form-horizontal" ng-repeat="hint in hints">
<div class="form-group">
<label for="htitle{{hint.id}}" class="col-xs-2 control-label">Titre</label>
<div class="col-xs-10">
<input type="text" id="htitle{{hint.id}}" ng-model="hint.title" class="form-control">
</div>
</div>
<div class="form-group">
<label for="hcnt{{hint.id}}" class="col-xs-2 control-label">Contenu</label>
<div class="col-xs-10">
<textarea class="form-control" id="hcnt{{hint.id}}" ng-model="hint.content"></textarea>
</div>
</div>
<div class="form-group">
<label for="hcost{{hint.id}}" class="col-xs-2 control-label">Coût</label>
<div class="col-xs-10">
<input type="text" id="hcost{{hint.id}}" ng-model="hint.cost" class="form-control" integer>
</div>
</div>
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button>
<a ng-click="deleteHint()" class="btn btn-danger" ng-show="hint.id"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a>
</form>
</div>
</div>
</div>
<div class="col-md-4" ng-controller="ExerciceFilesController">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Téléchargements<a ng-click="addFile()" class="pull-right btn btn-xs btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a></h3>
</div>
<div class="list-group">
<form ng-submit="saveFile()" class="list-group-item form" ng-repeat="file in files">
<input type="text" ng-model="file.name">
<a href="{{file.path}}" class="btn btn-default"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></a>
<a ng-click="deleteFile()" class="btn btn-danger"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a>
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button><br>
Taille : <span title="{{ file.size }} octets">{{ file.size | size }}</span><br>
SHA-1 : <samp>{{ file.checksum | cksum }}</samp>
</form>
</div>
</div>
</div>
<div class="col-md-4" ng-controller="ExerciceKeysController">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Drapeaux<a ng-click="addKey()" class="pull-right btn btn-xs btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a></h3>
</div>
<div class="list-group">
<form ng-submit="saveKey()" class="list-group-item form-horizontal" ng-repeat="key in keys">
<div class="form-group">
<label for="ktype{{key.id}}" class="col-xs-2 control-label">Intitulé</label>
<div class="col-xs-8">
<input type="text" id="ktype{{key.id}}" ng-model="key.type" class="form-control">
</div>
<div class="col-xs-1" ng-show="key.id">
<a ng-click="deleteKey()" class="btn btn-danger"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a>
</div>
</div>
<div class="form-group" ng-show="key.id">
<label for="kvalue{{key.id}}" class="col-xs-2 control-label">Hash</label>
<div class="col-xs-8">
<input type="text" id="kvalue{{key.id}}" ng-model="key.value" class="form-control">
</div>
<div class="col-xs-1">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button>
</div>
</div>
<div class="form-group" ng-show="!key.id">
<label for="kvalue{{key.id}}" class="col-xs-2 control-label">Clef brute</label>
<div class="col-xs-8">
<input type="text" id="kvalue{{key.id}}" ng-model="key.key" class="form-control">
</div>
<div class="col-xs-1">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,129 @@
<form ng-submit="saveScenes()" class="form-horizontal">
<h2>Interface publique<a ng-click="clearScene()" class="pull-right btn btn-danger"><span class="glyphicon glyphicon-remove-sign" aria-hidden="true"></span> Vider la scène</a><a ng-click="addScene()" class="pull-right btn btn-primary" style="margin-right: 10px"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un élément</a><button type="submit" style="margin-right: 10px" class="pull-right btn btn-success"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Publier cette scène</button>
</h2>
<div class="well" ng-repeat="scene in scenes">
<div class="form-group">
<div class="col-sm-offset-2 col-sm-6">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="scene.params.hide"> Masquer temporairement
</label>
</div>
</div>
<div class="col-sm-2">
<a ng-click="upScene(scene)" class="pull-right btn btn-default"><span class="glyphicon glyphicon-chevron-up" aria-hidden="true"></span> Up</a>
<a ng-click="downScene(scene)" class="pull-right btn btn-default"><span class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span> Down</a>
</div>
<div class="col-sm-2">
<a ng-click="delScene(scene)" class="pull-right btn btn-warning"><span class="glyphicon glyphicon-minus" aria-hidden="true"></span> Supprimer</a>
</div>
</div>
<div class="form-group">
<label for="type" class="col-sm-2 control-label">Type de scène</label>
<div class="col-sm-10">
<select class="form-control" id="type" ng-model="scene.type" ng-options="k as v for (k, v) in types"></select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'welcome'">
<label for="wtype" class="col-sm-2 control-label">Sorte</label>
<div class="col-sm-10">
<select class="form-control" id="wtype" ng-model="scene.params.kind" ng-options="k as v for (k, v) in welcome_types"></select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'panel'">
<label for="ptype" class="col-sm-2 control-label">Type de cadre</label>
<div class="col-sm-10">
<select class="form-control" id="ptype" ng-model="scene.params.kind" ng-options="k as v for (k, v) in panel_types"></select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'message' || scene.type == 'panel'">
<label for="mtitle" class="col-sm-2 control-label">Titre</label>
<div class="col-sm-10">
<input type="text" id="mtitle" ng-model="scene.params.title" class="form-control">
</div>
</div>
<div class="form-group" ng-if="scene.type == 'message'">
<label for="mlead" class="col-sm-2 control-label">Lead</label>
<div class="col-sm-10">
<input type="text" id="mlead" ng-model="scene.params.lead" class="form-control">
</div>
</div>
<div class="form-group" ng-if="scene.type == 'message' || scene.type == 'panel'">
<label for="mcnt" class="col-sm-2 control-label">Contenu HTML</label>
<div class="col-sm-10">
<textarea class="form-control" id="mcnt" ng-model="scene.params.html"></textarea>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'exercice'">
<label for="eex" class="col-sm-2 control-label">Exercice</label>
<div class="col-sm-10">
<select class="form-control" id="eex" ng-model="scene.params.exercice" ng-options="ex.id as ex.title for ex in exercices">
</select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'rank'">
<label for="rtype" class="col-sm-2 control-label">Sorte</label>
<div class="col-sm-10">
<select class="form-control" id="rtype" ng-model="scene.params.which" ng-options="k as v for (k, v) in rank_types"></select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'rank'">
<label for="rlimit" class="col-sm-2 control-label">Nombre d'éléments</label>
<div class="col-sm-10">
<input type="text" id="rlimit" ng-model="scene.params.limit" class="form-control" integer>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'rank'">
<label for="begin" class="col-sm-2 control-label">Début du classement (à partir de 0)</label>
<div class="col-sm-10">
<input type="text" id="rbegin" ng-model="scene.params.begin" class="form-control" integer>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'table'">
<label for="ttable" class="col-sm-2 control-label">Quelle table ?</label>
<div class="col-sm-10">
<select class="form-control" id="ttable" ng-model="scene.params.kind" ng-options="k as v for (k, v) in table_types">
</select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'table'">
<label for="ttheme" class="col-sm-2 control-label">Thèmes à afficher</label>
<div class="col-sm-10">
<select class="form-control" id="ttheme" multiple="1" ng-model="scene.params.themes" ng-options="th.id as th.name for th in themes">
</select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'table' && scene.params.kind == 'teams'">
<label for="tteams" class="col-sm-2 control-label">Équipes à afficher</label>
<div class="col-sm-10">
<select class="form-control" id="tteams" multiple="1" ng-model="scene.params.teams" ng-options="t.id as (t.rank + 'e - ' + t.name) for t in teams">
</select>
</div>
</div>
<div class="form-group" ng-if="scene.type == 'table'">
<div class="col-sm-offset-2 col-sm-6">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="scene.params.total"> Ligne de total
</label>
</div>
</div>
</div>
</div>
</form>

View file

@ -0,0 +1,131 @@
<h2>Paramètres<a ng-click="regenerate()" class="pull-right btn btn-info" role="button"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Regénérer les fichiers statiques</a></h2>
<form ng-submit="saveSettings()" class="form-horizontal well">
<input type="hidden" class="form-control" id="lastRegeneration" ng-model="config.generation">
<div class="form-group">
<label for="challengeName" class="col-sm-2 control-label">Nom du challenge</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="challengeName" ng-model="config.title">
</div>
</div>
<div class="form-group">
<label for="challengeAuthors" class="col-sm-2 control-label">Auteurs du challenge</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="challengeAuthors" ng-model="config.authors">
</div>
</div>
<div class="form-group">
<label for="startTime" class="col-sm-2 control-label">Début du challenge</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="startTime" ng-model="config.start">
</div>
<div class="col-sm-2 text-right">
<a ng-click="launchChallenge()" class="btn btn-warning" role="button"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Lancer le challenge</a>
</div>
</div>
<div class="form-group">
<label for="endTime" class="col-sm-2 control-label">Fin du challenge</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="endTime" ng-model="config.end">
</div>
<div class="col-sm-1 text-right">
<label for="duration" class="control-label">Durée</label>
</div>
<div class="col-sm-2">
<div class="input-group">
<input type="text" class="form-control" id="duration" ng-model="duration" integer>
<div class="input-group-addon">min</div>
</div>
</div>
</div>
<hr>
<div class="form-group">
<label for="firstBlood" class="col-sm-2 control-label">Bonus premier sang</label>
<div class="col-sm-1">
<input type="text" class="form-control" id="firstBlood" ng-model="config.firstBlood" float>
</div>
</div>
<div class="form-group">
<label for="submissionCostBase" class="col-sm-2 control-label">Coût de base d'une soumission</label>
<div class="col-sm-1">
<input type="text" class="form-control" id="submissionCostBase" ng-model="config.submissionCostBase" float>
</div>
</div>
<hr>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.allowRegistration"> Activer les inscriptions
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.denyNameChange"> Interdire les changements de nom d'équipe
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.enableResolutionRoute"> Activer la route montrant les solutions
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.partialValidation"> Activer la validation partielle des challenges
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.enableExerciceDepend"> Activer les dépendances des exercices
</label>
</div>
</div>
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Propager ces paramètres</button>
</div>
</form>
<div class="well">
<div class="col-sm-4 center">
<a ng-click="reset('challenges')" class="btn btn-warning" role="button"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Effacer les challenges et les thèmes</a>
</div>
<div class="col-sm-4 center">
<a ng-click="reset('teams');" class="btn btn-warning" role="button"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Effacer les équipes</a>
</div>
<div class="col-sm-4 center">
<a ng-click="reset('game');" class="btn btn-warning" role="button"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Effacer la partie (tentatives, indices, ...)</a>
</div>
<div class="clearfix"></div>
</div>

View file

@ -0,0 +1,86 @@
<h1>
{{ team.name }}
<span ng-show="team.name != team.initialName"> ({{ team.initialName}})</span>
<a ng-click="showStats()" class="pull-right btn btn-primary" style="margin-right: 10px" ng-if="team.id">
<span class="glyphicon glyphicon-list" aria-hidden="true"></span>
Statistiques
</a>
</h1>
<form ng-submit="saveTeam()" class="form-horizontal">
<div class="form-group">
<label for="idTeam" class="col-sm-2 control-label">Identifiant</label>
<div class="col-sm-10">
{{ team.id }}
</div>
</div>
<div class="form-group">
<label for="initialName" class="col-sm-2 control-label">Nom initial</label>
<div class="col-sm-10">
{{ team.initialName }}
</div>
</div>
<div class="form-group" ng-repeat="field in fields">
<label for="{{ field }}" class="col-sm-2 control-label">{{ field | capitalize }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="{{ field }}" ng-model="team[field]" ng-if="field != 'color'">
<input type="color" class="form-control" id="{{ field }}{{ member.id }}" ng-model="team[field]" ng-if="field == 'color'" color>
</div>
</div>
<div class="text-right" ng-show="team.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button>
<a class="btn btn-danger" ng-click="deleteTeam()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</a>
</div>
<div class="text-right" ng-show="!team.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create theme</button>
</div>
</form>
<hr>
<div class="col-sm-5">
<div class="panel panel-primary">
<div class="panel-heading">
<div class="panel-title">
<span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
Certificate
<span class="label label-success" ng-if="hasCertificate">Generated</span>
<span class="label label-danger" ng-if="!hasCertificate">Not found</span>
</div>
</div>
<div class="panel-body">
<a ng-click="generateCertificate()" class="btn btn-success" ng-if="!hasCertificate">
<span class="glyphicon glyphicon-certificate" aria-hidden="true"></span> Generate certificate</a>
<a ng-click="revokeCertificate()" class="btn btn-danger" ng-if="hasCertificate">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Revoke certificate</a>
</div>
</div>
</div>
<form ng-submit="saveTeamMembers()" class="col-sm-7 form-horizontal" ng-if="team.id" ng-controller="TeamMembersController">
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-title">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> Membres
<button type="submit" class="pull-right btn btn-xs btn-primary" style="margin-left: 10px">
<span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save members</button>
<a ng-click="newMember()" class="pull-right btn btn-xs btn-default">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add member</a>
</div>
</div>
<div class="panel-body" ng-if="members.length == 0">
This team has no member!
</div>
<div class="list-group-item" ng-repeat="member in members">
<div class="form-group" ng-repeat="field in fields">
<label for="{{ field }}{{ member.id }}" class="col-sm-2 control-label">{{ field | capitalize }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="{{ field }}{{ member.id }}" ng-model="member[field]">
</div>
<div class="col-sm-1" ng-if="$first">
<a ng-click="removeMember(member)" class="pull-right btn btn-primary"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a>
</div>
</div>
</div>
</div>
</form>

View file

@ -1,3 +1,9 @@
<h2>
&Eacute;quipes
<a ng-click="show('new')" class="pull-right btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une équipe</a>
<a ng-click="show('print')" class="pull-right btn btn-default"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Imprimer les équipes</a>
</h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query"></p>
<table class="table table-hover table-bordered">
<thead>

View file

@ -0,0 +1,32 @@
<h2 ng-controller="SettingsController">{{ config.title }} &ndash; &Eacute;quipes</h2>
<p class="hidden-print"><input type="search" class="form-control" placeholder="Search" ng-model="query"></p>
<table class="table table-hover table-bordered">
<thead>
<tr>
<th ng-repeat="field in fields">
{{ field }}
</th>
<th>
members
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="team in teams | filter: query" ng-click="show(team.id)">
<td ng-repeat="field in fields">
{{ team[field] }}
</td>
<td ng-controller="TeamMembersController" style="padding: 0;">
<table class="table table-hover table-condensed" style="margin: 0;">
<tr class="row" ng-repeat="member in members">
<td class="col-sm-3" ng-repeat="field in fields">
{{ member[field] }}
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>

View file

@ -9,7 +9,7 @@
.RdYlGn .q7-8{fill:rgb(70,80,80)}
</style>
<h1>{{ team.name }}<span ng-show="team.name != team.initialName"> ({{ team.initialName}})</span> <small><span ng-repeat="member in members"><span ng-show="$last && !$first"> et </span><span ng-show="$middle">, </span>{{ member.firstname | capitalize }} <em ng-show="member.nickname">{{ member.nickname }}</em> {{ member.lastname | capitalize }}</span></small></h1>
<h1>{{ team.name }}<span ng-if="team.name != team.initialName"> ({{ team.initialName}})</span> <small><span ng-repeat="member in members"><span ng-if="$last && !$first"> et </span><span ng-if="$middle">, </span>{{ member.firstname | capitalize }} <em ng-if="member.nickname">{{ member.nickname }}</em> {{ member.lastname | capitalize }}</span></small></h1>
<div ng-controller="TeamExercicesController">
@ -32,7 +32,7 @@
<dt>{{ theme.name }}</dt>
<dd>
<ul class="list-unstyled">
<li ng-repeat="(eid,exercice) in theme.exercices" ng-show="my.exercices[eid] && my.exercices[eid].solved"><a href="https://fic.srs.epita.fr/{{ my.exercices[eid].theme_id }}/{{ eid }}" target="_blank"><abbr title="{{ my.exercices[eid].statement }}">{{ exercice.title }}</abbr></a> (<abbr title="{{ my.exercices[eid].solved_time | date:'mediumDate' }} à {{ my.exercices[eid].solved_time | date:'mediumTime' }}">{{ my.exercices[eid].solved_number }}<sup>e</sup></abbr>)</li>
<li ng-repeat="(eid,exercice) in theme.exercices" ng-if="my.exercices[eid] && my.exercices[eid].solved_rank"><a href="/{{ my.exercices[eid].theme_id }}/{{ eid }}" target="_blank"><abbr title="{{ my.exercices[eid].statement }}">{{ exercice.title }}</abbr></a> (<abbr title="{{ my.exercices[eid].solved_time | date:'mediumDate' }} à {{ my.exercices[eid].solved_time | date:'mediumTime' }}">{{ my.exercices[eid].solved_rank }}<sup>e</sup></abbr>)</li>
</ul>
</dd>
</div>

View file

@ -1,3 +1,5 @@
<h2>Thèmes<a ng-click="show('new')" class="pull-right btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un thème</a></h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query"></p>
<table class="table table-hover table-bordered">
<thead>

View file

@ -1,12 +1,24 @@
<form class="form" ng-submit="ssubmit()">
<h2>{{theme.name}} <small>{{theme.authors}}</small></h2>
<form ng-submit="saveTheme()" class="form-horizontal">
<div class="form-group" ng-repeat="field in fields">
<label for="{{ field }}">{{ field }}</label>
<input type="text" class="form-control" id="{{ field }}" ng-model="theme[field]">
<label for="{{ field }}" class="col-sm-1 control-label">{{ field | capitalize }}</label>
<div class="col-sm-11">
<input type="text" class="form-control" id="{{ field }}" ng-model="theme[field]">
</div>
</div>
<div class="text-right" ng-show="theme.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button>
<a class="btn btn-danger" ng-click="deleteTheme()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</a>
</div>
<div class="text-right" ng-show="!theme.id">
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create theme</button>
</div>
<button class="btn btn-primary" ng-click="saveTheme()">Save</button>
</form>
<div ng-controller="ExercicesListController">
<div ng-show="theme.id" ng-controller="ExercicesListController">
<h3>Exercices<a ng-click="show('new')" class="pull-right btn btn-sm btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un exercice</a></h3>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query"></p>
<table class="table table-hover table-bordered">
<thead>
@ -17,9 +29,9 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="team in teams | filter: query" ng-click="show(team.id)">
<tr ng-repeat="exercice in exercices | filter: query" ng-click="show(exercice.id)">
<td ng-repeat="field in fields">
{{ team[field] }}
{{ exercice[field] }}
</td>
</tr>
</tbody>

25
admin/time.go Normal file
View file

@ -0,0 +1,25 @@
package main
import (
"fmt"
"net/http"
"path"
"srs.epita.fr/fic-server/admin/api"
"srs.epita.fr/fic-server/frontend/time"
"srs.epita.fr/fic-server/settings"
"github.com/julienschmidt/httprouter"
)
func init() {
api.Router().GET("/time.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if config, err := settings.ReadSettings(path.Join(api.TeamsDir, settings.SettingsFile)); err != nil {
http.Error(w, fmt.Sprintf("{\"errmsg\":\"%q\"}", err), http.StatusInternalServerError)
} else {
time.ChallengeStart = config.Start
time.ChallengeEnd = config.End
time.TimeHandler{}.ServeHTTP(w, r)
}
})
}

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path"
@ -99,34 +100,30 @@ func genThemesFile() error {
return nil
}
func genTeamAll(team fic.Team) error {
func genTeamAll(team fic.Team) {
if err := genThemesFile(); err != nil {
return err
log.Println("themes.json generation error: ", err)
} else if err := genTeamsFile(); err != nil {
return err
log.Println("teams.json generation error: ", err)
} else if err := genTeamMyFile(team); err != nil {
return err
log.Println("my.json(", team.Id, ") generation error: ", err)
}
return nil
}
func genAll() error {
func genAll() {
if err := genThemesFile(); err != nil {
return err
log.Println("themes.json generation error: ", err)
} else if err := genTeamsFile(); err != nil {
return err
log.Println("teams.json generation error: ", err)
} else if err := genMyPublicFile(); err != nil {
return err
log.Println("MyPublic generation error: ", err)
} else if teams, err := fic.GetTeams(); err != nil {
return err
log.Println("Team retrieval error: ", err)
} else {
for _, team := range(teams) {
if err := genTeamMyFile(team); err != nil {
return err
log.Println("Tean generation error: ", err)
}
}
}
return nil
}

48
backend/hint.go Normal file
View file

@ -0,0 +1,48 @@
package main
import (
"encoding/json"
"fmt"
"log"
"io/ioutil"
"os"
"srs.epita.fr/fic-server/libfic"
)
type askOpenHint struct {
HintId int64 `json:"id"`
}
func treatOpeningHint(pathname string, team fic.Team) {
var ask askOpenHint
if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
log.Println("[ERR]", err)
} else if err := json.Unmarshal(cnt_raw, &ask); err != nil {
log.Println("[ERR]", err)
} else if ask.HintId == 0 {
log.Println("[WRN] Invalid content in hint file: ", pathname)
os.Remove(pathname)
} else if hint, err := fic.GetHint(ask.HintId); err != nil {
log.Println("[ERR]", err)
} else if err := team.OpenHint(hint); err != nil {
log.Println("[ERR]", err)
} else {
// Write event
if exercice, err := hint.GetExercice(); err != nil {
log.Println("[WRN]", err)
} else if lvl, err := exercice.GetLevel(); err != nil {
log.Println("[WRN]", err)
} else if theme, err := exercice.GetTheme(); err != nil {
log.Println("[WRN]", err)
} else if _, err := fic.NewEvent(fmt.Sprintf("L'équipe %s a dévoilé un indice pour le <strong>%d<sup>e</sup></strong> challenge %s !", team.Name, lvl, theme.Name), "alert-info"); err != nil {
log.Println("[WRN] Unable to create event:", err)
}
genTeamMyFile(team)
if err := os.Remove(pathname); err != nil {
log.Println("[ERR]", err)
}
}
}

View file

@ -11,16 +11,18 @@ import (
"strings"
"time"
"golang.org/x/exp/inotify"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"gopkg.in/fsnotify.v1"
)
var TeamsDir string
var SubmissionDir string
func watchsubdir(watcher *inotify.Watcher, pathname string) error {
func watchsubdir(watcher *fsnotify.Watcher, pathname string) error {
log.Println("Watch new directory:", pathname)
if err := watcher.AddWatch(pathname, inotify.IN_CLOSE_WRITE|inotify.IN_CREATE); err != nil {
if err := watcher.Add(pathname); err != nil {
return err
}
@ -41,16 +43,39 @@ func watchsubdir(watcher *inotify.Watcher, pathname string) error {
}
}
var lastRegeneration time.Time
func reloadSettings(config settings.FICSettings) {
if lastRegeneration != config.Generation || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallenges != !config.EnableExerciceDepend || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase {
lastRegeneration = config.Generation
fic.PartialValidation = config.PartialValidation
fic.UnlockedChallenges = !config.EnableExerciceDepend
fic.FirstBlood = config.FirstBlood
fic.SubmissionCostBase = config.SubmissionCostBase
log.Println("Generating files...")
go func() {
genAll()
log.Println("Full generation done")
}()
} else {
log.Println("No change found. Skipping regeneration.")
}
}
func main() {
var dsn = flag.String("dsn", "fic:fic@/fic", "DSN to connect to the MySQL server")
flag.StringVar(&SubmissionDir, "submission", "./submissions", "Base directory where save submissions")
flag.StringVar(&TeamsDir, "teams", "../TEAMS", "Base directory where save teams JSON files")
var skipFullGeneration = flag.Bool("skipFullGeneration", false, "Skip initial full generation (safe to skip after start)")
flag.StringVar(&SubmissionDir, "./submission", "./submissions", "Base directory where save submissions")
flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
flag.StringVar(&fic.FilesDir, "files", "/files", "Request path prefix to reach files")
flag.Parse()
log.SetPrefix("[backend] ")
SubmissionDir = path.Clean(SubmissionDir)
TeamsDir = path.Clean(TeamsDir)
rand.Seed(time.Now().UnixNano())
@ -67,35 +92,32 @@ func main() {
}
defer fic.DBClose()
// Load configuration
settings.LoadAndWatchSettings(path.Join(TeamsDir, settings.SettingsFile), reloadSettings)
log.Println("Registering directory events...")
watcher, err := inotify.NewWatcher()
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
if err := watchsubdir(watcher, SubmissionDir); err != nil {
log.Fatal(err)
}
if !*skipFullGeneration {
log.Println("Generating files...")
go genAll()
}
for {
select {
case ev := <-watcher.Event:
if ev.Mask&inotify.IN_CREATE == inotify.IN_CREATE {
case ev := <-watcher.Events:
if d, err := os.Stat(ev.Name); err == nil && d.IsDir() && ev.Op & fsnotify.Create == fsnotify.Create {
// Register new subdirectory
if d, err := os.Stat(ev.Name); err == nil && d.IsDir() {
if err := watchsubdir(watcher, ev.Name); err != nil {
log.Println(err)
}
if err := watchsubdir(watcher, ev.Name); err != nil {
log.Println(err)
}
} else if ev.Mask&inotify.IN_CLOSE_WRITE == inotify.IN_CLOSE_WRITE {
} else if ev.Op & fsnotify.Write == fsnotify.Write {
go treat(ev.Name)
}
case err := <-watcher.Error:
case err := <-watcher.Errors:
log.Println("error:", err)
}
}
@ -112,6 +134,8 @@ func treat(raw_path string) {
log.Println("[ERR]", err)
} else if spath[2] == "name" {
treatRename(raw_path, team)
} else if spath[2] == "hint" {
treatOpeningHint(raw_path, team)
} else {
treatSubmission(raw_path, team, spath[2])
}

47
frontend/chname.go Normal file
View file

@ -0,0 +1,47 @@
package main
import (
"log"
"net/http"
"strings"
)
var denyNameChange bool = true
type ChNameHandler struct {}
func (n ChNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if denyNameChange {
log.Printf("UNHANDELED %s name change request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
http.Error(w, "{\"errmsg\":\"Le changement de nom est prohibé.\"}", http.StatusForbidden)
return
}
log.Printf("Handling %s name change request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
// Check request type and size
if r.Method != "POST" {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return
} else if r.ContentLength < 0 || r.ContentLength > 1023 {
http.Error(w, "{\"errmsg\":\"Requête trop longue ou de taille inconnue\"}", http.StatusRequestEntityTooLarge)
return
}
// Extract URL arguments
var sURL = strings.Split(r.URL.Path, "/")
if len(sURL) != 1 {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return
}
team := sURL[0]
// Enqueue file for backend treatment
if saveTeamFile(SubmissionDir, team, "name", w, r) {
http.Error(w, "{\"errmsg\":\"Demande de changement de nom acceptée\"}", http.StatusAccepted)
}
}

39
frontend/hint.go Normal file
View file

@ -0,0 +1,39 @@
package main
import (
"log"
"net/http"
"strings"
)
type HintHandler struct {}
func (h HintHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("Handling %s opening hint request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "application/json")
// Check request type and size
if r.Method != "POST" {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return
} else if r.ContentLength <= 0 || r.ContentLength > 1023 {
http.Error(w, "{\"errmsg\":\"Requête trop longue ou de taille inconnue\"}", http.StatusRequestEntityTooLarge)
return
}
// Extract URL arguments
var sURL = strings.Split(r.URL.Path, "/")
if len(sURL) != 1 && len(sURL) != 2 {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return
}
team := sURL[0]
// Enqueue file for backend treatment
if saveTeamFile(SubmissionDir, team, "hint", w, r) {
http.Error(w, "{\"errmsg\":\"Demande d'astuce acceptée...\"}", http.StatusAccepted)
}
}

View file

@ -1,7 +1,6 @@
package main
import (
"bufio"
"flag"
"fmt"
"log"
@ -9,68 +8,90 @@ import (
"os"
"path"
"time"
fronttime "srs.epita.fr/fic-server/frontend/time"
"srs.epita.fr/fic-server/settings"
)
const startedFile = "started"
var TeamsDir string
var SubmissionDir string
var TmpSubmissionDir string
func touchStartedFile(startSub time.Duration) {
time.Sleep(startSub)
var touchTimer *time.Timer = nil
func touchStartedFile() {
if fd, err := os.Create(path.Join(TeamsDir, startedFile)); err == nil {
log.Println("Started! Go, Go, Go!!")
fd.Close()
} else {
log.Println("Unable to start challenge:", err)
log.Fatal("Unable to start challenge:", err)
}
}
func reloadSettings(config settings.FICSettings) {
if fronttime.ChallengeStart != config.Start || fronttime.ChallengeEnd != config.End {
if touchTimer != nil {
touchTimer.Stop()
}
startSub := config.Start.Sub(time.Now())
if startSub > 0 {
log.Println("Challenge will starts at", config.Start, "in", startSub)
if _, err := os.Stat(path.Join(TeamsDir, startedFile)); !os.IsNotExist(err) {
os.Remove(path.Join(TeamsDir, startedFile))
}
touchTimer = time.AfterFunc(config.Start.Sub(time.Now().Add(time.Duration(1 * time.Second))), touchStartedFile)
} else {
log.Println("Challenge started at", config.Start, "since", -startSub)
touchStartedFile()
}
log.Println("Challenge ends on", config.End)
fronttime.ChallengeStart = config.Start
fronttime.ChallengeEnd = config.End
} else {
log.Println("Configuration reloaded, but start/end times doesn't change.")
}
enableResolutionRoute = config.EnableResolutionRoute
denyNameChange = config.DenyNameChange
allowRegistration = config.AllowRegistration
}
func main() {
var bind = flag.String("bind", "0.0.0.0:8080", "Bind port/socket")
var bind = flag.String("bind", "127.0.0.1:8080", "Bind port/socket")
var prefix = flag.String("prefix", "", "Request path prefix to strip (from proxy)")
var start = flag.Int64("start", 0, fmt.Sprintf("Challenge start timestamp (in 2 minutes: %d)", time.Now().Unix()/60*60+120))
var duration = flag.Duration("duration", 180*time.Minute, "Challenge duration")
var denyChName = flag.Bool("denyChName", false, "Deny team to change their name")
var allowRegistration = flag.Bool("allowRegistration", false, "New team can add itself")
flag.StringVar(&TeamsDir, "teams", "../TEAMS", "Base directory where save teams JSON files")
flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
flag.StringVar(&SubmissionDir, "submission", "./submissions/", "Base directory where save submissions")
flag.Parse()
log.SetPrefix("[frontend] ")
SubmissionDir = path.Clean(SubmissionDir)
TmpSubmissionDir = path.Join(SubmissionDir, ".tmp")
log.Println("Creating submission directory...")
if _, err := os.Stat(SubmissionDir); os.IsNotExist(err) {
if err := os.MkdirAll(SubmissionDir, 0777); err != nil {
if _, err := os.Stat(TmpSubmissionDir); os.IsNotExist(err) {
if err := os.MkdirAll(TmpSubmissionDir, 0777); err != nil {
log.Fatal("Unable to create submission directory: ", err)
}
}
startTime := time.Unix(*start, 0)
startSub := startTime.Sub(time.Now())
end := startTime.Add(*duration).Add(time.Duration(1 * time.Second))
// Load configuration
settings.LoadAndWatchSettings(path.Join(TeamsDir, settings.SettingsFile), reloadSettings)
log.Println("Challenge ends on", end)
if startSub > 0 {
log.Println("Challenge starts at", startTime, "in", startSub)
fmt.Printf("PRESS ENTER TO LAUNCH THE COUNTDOWN ")
bufio.NewReader(os.Stdin).ReadLine()
if _, err := os.Stat(path.Join(TeamsDir, startedFile)); !os.IsNotExist(err) {
os.Remove(path.Join(TeamsDir, startedFile))
}
go touchStartedFile(startTime.Sub(time.Now().Add(time.Duration(1 * time.Second))))
} else {
log.Println("Challenge started at", startTime, "since", -startSub)
go touchStartedFile(time.Duration(0))
}
log.Println("Registering handlers...")
http.Handle(fmt.Sprintf("%s/time.json", *prefix), http.StripPrefix(*prefix, TimeHandler{startTime, *duration}))
http.Handle(fmt.Sprintf("%s/", *prefix), http.StripPrefix(*prefix, SubmissionHandler{end, *denyChName, *allowRegistration}))
// Register handlers
http.Handle(fmt.Sprintf("%s/chname/", *prefix), http.StripPrefix(fmt.Sprintf("%s/chname/", *prefix), ChNameHandler{}))
http.Handle(fmt.Sprintf("%s/openhint/", *prefix), http.StripPrefix(fmt.Sprintf("%s/openhint/", *prefix), HintHandler{}))
http.Handle(fmt.Sprintf("%s/registration", *prefix), http.StripPrefix(fmt.Sprintf("%s/registration", *prefix), RegistrationHandler{}))
http.Handle(fmt.Sprintf("%s/resolution/", *prefix), http.StripPrefix(fmt.Sprintf("%s/resolution/", *prefix), ResolutionHandler{}))
http.Handle(fmt.Sprintf("%s/submission/", *prefix), http.StripPrefix(fmt.Sprintf("%s/submission/", *prefix), SubmissionHandler{}))
http.Handle(fmt.Sprintf("%s/time.json", *prefix), http.StripPrefix(*prefix, fronttime.TimeHandler{}))
// Serve pages
log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
if err := http.ListenAndServe(*bind, nil); err != nil {
log.Fatal("Unable to listen and serve: ", err)

View file

@ -0,0 +1,34 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name fic.srs.epita.fr;
access_log /var/log/nginx/fic2016.access_log main;
error_log /var/log/nginx/fic2016.error_log info;
root /srv/www/fic2016-static/;
error_page 403 404 /e404.html;
error_page 413 404 /e413.html;
error_page 500 502 504 /e500.html;
location /.htaccess {
return 404;
}
location /chbase.sh {
return 404;
}
location ~ ^/[0-9] {
rewrite ^/.*$ /index.html;
}
location /edit {
rewrite ^/.*$ /index.html;
}
location /rank {
rewrite ^/.*$ /index.html;
}
}

41
frontend/register.go Normal file
View file

@ -0,0 +1,41 @@
package main
import (
"log"
"net/http"
"path"
)
var allowRegistration bool = false
type RegistrationHandler struct {}
func (e RegistrationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if !allowRegistration {
log.Printf("UNHANDLED %s registration request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
http.Error(w, "{\"errmsg\":\"L'enregistrement d'équipe n'est pas permis.\"}", http.StatusForbidden)
return
}
log.Printf("Handling %s registration request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
// Check request type and size
if r.Method != "POST" {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return
} else if r.ContentLength < 0 || r.ContentLength > 1023 {
http.Error(w, "{\"errmsg\":\"Requête trop longue ou de taille inconnue\"}", http.StatusRequestEntityTooLarge)
return
}
// Enqueue file for backend treatment
if err := saveFile(path.Join(SubmissionDir, "_registration"), "", r); err != nil {
log.Println("Unable to open registration file:", err)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError)
return
}
http.Error(w, "{\"errmsg\":\"Demande d'enregistrement acceptée\"}", http.StatusAccepted)
}

42
frontend/resolution.go Normal file
View file

@ -0,0 +1,42 @@
package main
import (
"log"
"net/http"
"path"
"text/template"
)
var enableResolutionRoute bool = false
type ResolutionHandler struct {}
const resolutiontpl = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Challenge Forensic - Résolution</title>
</head>
<body style="margin: 0">
<video src="{{.}}" controls width="100%" height="100%"></video>
</body>
</html>
`
func (s ResolutionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !enableResolutionRoute {
log.Printf("UNHANDELED %s request from %s: /resolution%s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
http.NotFound(w, r)
return
}
log.Printf("Handling %s request from %s: /resolution%s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "text/html")
if resolutionTmpl, err := template.New("resolution").Parse(resolutiontpl); err != nil {
log.Println("Cannot create template: ", err)
} else if err := resolutionTmpl.Execute(w, path.Join("/vids/", r.URL.Path)); err != nil {
log.Println("An error occurs during template execution: ", err)
}
}

69
frontend/save.go Normal file
View file

@ -0,0 +1,69 @@
package main
import (
"bufio"
"io/ioutil"
"log"
"net/http"
"os"
"path"
)
func saveTeamFile(dirname string, team string, filename string, w http.ResponseWriter, r *http.Request) bool {
if _, err := os.Stat(path.Join(dirname, team)); os.IsNotExist(err) || len(team) < 1 || team[0] == '_' {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return false
} else {
if len(filename) <= 0 {
log.Println("EMPTY $EXERCICE RECEIVED:", filename)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError)
return false
}
// Previous submission not treated
if _, err := os.Stat(path.Join(SubmissionDir, team, filename)); !os.IsNotExist(err) {
http.Error(w, "{\"errmsg\":\"Du calme ! une requête est déjà en cours de traitement.\"}", http.StatusPaymentRequired)
return false
}
if err := saveFile(path.Join(SubmissionDir, team), filename, r); err != nil {
log.Println("Unable to handle submission file:", err)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError)
return false
}
return true
}
}
func saveFile(dirname string, filename string, r *http.Request) error {
if _, err := os.Stat(dirname); os.IsNotExist(err) {
if err := os.MkdirAll(dirname, 0777); err != nil {
return err
}
}
// Write content to temp file
tmpfile, err := ioutil.TempFile(TmpSubmissionDir, "")
if err != nil {
return err
}
writer := bufio.NewWriter(tmpfile)
reader := bufio.NewReader(r.Body)
if _, err := reader.WriteTo(writer); err != nil {
return err
}
writer.Flush()
tmpfile.Close()
if filename == "" {
filename = path.Base(tmpfile.Name())
}
if err := os.Rename(tmpfile.Name(), path.Join(dirname, filename)); err != nil {
log.Println("[ERROR] Unable to move file: ", err)
}
return nil
}

View file

@ -1,10 +1,42 @@
@font-face {
font-family: "Linux Biolinum";
src: url('../fonts/LinBiolinum_R.woff') format('woff');
}
@font-face {
font-family: "Linux Biolinum";
src: url('../fonts/LinBiolinum_RB.woff') format('woff');
font-weight: bold;
}
@font-face {
font-family: "Linux Biolinum";
src: url('../fonts/LinBiolinum_RI.woff') format('woff');
font-style: italic;
}
[ng-cloak] {
display:none !important;
}
body {
overflow-y: scroll;
}
.beautiful {
font-family: "Linux Biolinum",Helvetica,Arial,sans-serif;
}
.beautiful ol {
font-size: 133%;
}
.beautiful ol ol {
font-size: 90%;
}
.text-bold {
font-weight: bolder;
}
.text-indent p {
text-indent: 1em;
}
.navbar {
margin-bottom: 0;
@ -15,7 +47,6 @@ body {
}
.navbar #clock {
font-size: 70px;
text-align: center;
}
.point, .expired {
transition: color text-shadow 1s;
@ -30,9 +61,15 @@ body {
.point {
text-shadow: 0 0 20px #0055ff;
}
.navbar-inverse .point {
text-shadow: 0 0 12px #0055ff;
}
.end .point {
text-shadow: 0 0 20px #ff5500;
}
.navbar-inverse .end .point {
text-shadow: 0 0 12px #ff5500;
}
@-webkit-keyframes clockanim {
0% { opacity: 1.0; }
50% { opacity: 0; }
@ -69,11 +106,6 @@ h1 small.authors {
}
.teamname {
padding: 2px 7px;
border-radius: 2px;
box-shadow: #444 0 0 3px;
}
.teamname span {
-webkit-filter: invert(100%);
filter: invert(100%);
}
@ -113,3 +145,53 @@ h1 small.authors {
transition-delay: 0.7s;
transition-duration: 0s;
}
.carousel-indicators {
bottom: -10px;
}
.carousel-caption {
padding: 0;
position: static;
}
.carousel .table {
margin-bottom: 0;
}
.carousel .table-condensed td {
padding: 2px;
}
.table th.frotated {
border: 0;
}
.table th.rotated {
height: 100px;
width: 40px;
min-width: 40px;
max-width: 40px;
position: relative;
vertical-align: bottom;
padding: 0;
font-size: 12px;
line-height: 0.9;
border: 0;
}
th.rotated > div {
position: relative;
top: 0px;
left: -50px;
height: 100%;
transform: skew(45deg,0deg);
overflow: hidden;
border: 1px solid #000;
}
th.rotated div span {
transform: skew(-45deg,0deg) rotate(45deg);
position: absolute;
bottom: 40px;
left: -35px;
display: inline-block;
width: 110px;
text-align: left;
text-overflow: ellipsis;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

BIN
frontend/static/img/rcc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Before After
Before After

View file

@ -22,90 +22,97 @@
</head>
<body>
<noscript>
<div class="alert alert-danger">
<strong>Veuillez activer le JavaScript.</strong> Ce site requiert un navigateur interprêtant le JavaScript pour fonctionner. Veuillez l'activer ou en télécharger un supportant cette technologie.
</div>
</noscript>
<div class="navbar navbar-default">
<div class="container">
<div class="row">
<div class="navbar-header col-sm-3" ng-show="!(time.start || my.team_id)">
<a href="https://www.forum-fic.com/">
<img src="/img/fic.png" alt="Forum International de la Cybersécurité" class="center-block">
</a>
</div>
<div class="navbar-header col-sm-3" ng-show="(time.start || my.team_id)">
<a href="/">
<img src="/img/fic.png" alt="Forum International de la Cybersécurité" class="center-block">
</a>
</div>
<div class="navbar-right col-sm-2">
<a href="http://www.epita.fr/">
<img src="/img/epita.png" alt="Epita" class="center-block">
</a>
</div>
<div id="clock" class="col-sm-7" ng-class="{expired: time.expired, end: time.end}" ng-show="time.start || my.team_id">
<span id="hours">{{ time.hours | time }}</span>
<span class="point">:</span>
<span id="min">{{ time.minutes | time }}</span>
<span class="point">:</span>
<span id="sec">{{ time.seconds | time }}</span>
</div>
<div id="clock" class="col-sm-7" ng-show="!(time.start || my.team_id)" style="padding: 25px">
<div class="btn-group btn-group-justified btn-group-lg">
<a class="btn btn-default" href="/">
<span class="glyphicon glyphicon-home"></span> Accueil
<div class="navbar-left col-md-3">
<a href="https://www.forum-fic.com/" ng-if="!(time.start || my.team_id)">
<img src="/img/fic.png" alt="Forum International de la Cybersécurité" class="center-block">
</a>
<a class="btn btn-default" href="/rank">
<span class="glyphicon glyphicon-list"></span> Classement
<a href="/" ng-if="(time.start || my.team_id)" ng-cloak>
<img src="/img/fic.png" alt="Forum International de la Cybersécurité" class="center-block">
</a>
<a class="btn btn-default" href="https://www.youtube.com/playlist?list=PLSJ8QLhKMtQv7jRhdAn9wXSMYTsvqfieX">
<span class="glyphicon glyphicon-blackboard"></span> Vidéos
</div>
<div id="clock" class="col-md-6 text-center" ng-hide="1">Chargement...</div>
<div id="clock" class="col-md-6 text-center" ng-class="{expired: time.expired, end: time.end}" ng-if="time.start || my.team_id" ng-cloak>
<span id="hours">{{ time.hours | time }}</span>
<span class="point">:</span>
<span id="min">{{ time.minutes | time }}</span>
<span class="point">:</span>
<span id="sec">{{ time.seconds | time }}</span>
</div>
<div id="clock" class="col-md-6" ng-if="!(time.start || my.team_id)" style="padding: 25px" ng-cloak>
<div class="btn-group btn-group-justified btn-group-lg">
<a class="btn btn-default" href="/">
<span class="glyphicon glyphicon-home"></span> Accueil
</a>
<a class="btn btn-default" href="/rank">
<span class="glyphicon glyphicon-list"></span> Classement
</a>
<a class="btn btn-default disabled" href="https://www.youtube.com/playlist?list=PLSJ8QLhKMtQv7jRhdAn9wXSMYTsvqfieX">
<span class="glyphicon glyphicon-blackboard"></span> Vidéos
</a>
</div>
</div>
<div class="col-md-3 text-right">
<a href="http://www.epita.fr/">
<img src="/img/epita.png" alt="Epita" class="center-block">
</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-controller="DataController">
<div class="row">
<div class="col-sm-9 col-sm-offset-3">
<div class="page-header">
<h1 ng-show="(current_theme)">{{ themes[current_theme].name }} <small class="authors" ng-show="themes[current_theme].authors">{{ themes[current_theme].authors }}</small></h1>
<h1 ng-show="(!current_theme && title)">{{ title }} <small class="authors" ng-show="authors">{{ authors }}</small></h1>
<h1 ng-show="(!current_theme && !title)">Challenge forensic 2016 <small class="authors">Laboratoire SRS, Epita</small></h1>
<div class="col-sm-3">
<div class="panel panel-default" ng-if="(my.team_id)" style="margin-top: 10px; margin-bottom: 0px;" ng-cloak>
<div class="panel-heading" style="background-color: {{ teams[my.team_id].color }}; color: {{ teams[my.team_id].color }};">
<a style="margin: -8px -13px; color: {{ teams[my.team_id].color }};" class="pull-right btn btn-default" href="/edit"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></a>
<div class="panel-title">
<strong class="teamname">{{ my.name }}</strong>
</div>
</div>
<div class="panel-body">
<span ng-if="teams[my.team_id].rank">{{ teams[my.team_id].rank }}<sup><sup><ng-pluralize count="teams[my.team_id].rank" when="{'one': 're', 'other': 'e'}"></ng-pluralize></sup></sup> sur {{ teams_count }} &ndash;</span>
<ng-pluralize count="my.score" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize>
<div style="margin: -8px -10px;" class="pull-right btn-group">
<a class="btn btn-default" href="/rules"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></a>
<a class="btn btn-default" href="/rank"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span></a>
</div>
</div>
</div>
</div>
<div class="col-sm-9">
<div class="page-header">
<h1 ng-if="(current_theme)" ng-cloak>{{ themes[current_theme].name }} <small class="authors" ng-if="themes[current_theme].authors">{{ themes[current_theme].authors }}</small></h1>
<h1 ng-if="(!current_theme && title)" ng-cloak>{{ title }} <small class="authors" ng-if="authors">{{ authors }}</small></h1>
<h1 ng-if="(!current_theme && !title)">{{ settings.title }} <small class="authors">{{ settings.authors }}</small></h1>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-3">
<div class="panel panel-default" ng-show="(my.team_id)">
<div class="panel-body">
<strong class="teamname" style="background-color: {{ teams[my.team_id].color }}; color: {{ teams[my.team_id].color }};"><span>{{ my.name }}</span></strong>
<a style="float: right;" class="btn btn-default btn-xs" href="/edit">voir</a><br><br>
<span ng-show="teams[my.team_id].rank">{{ teams[my.team_id].rank }}<sup>e</sup> sur {{ teams_count }} &ndash;</span>
{{ my.score }} points
<a style="float: right;" class="btn btn-default btn-xs btn-primary" href="/rank">classement</a>
</div>
<div class="list-group" ng-cloak>
<a ng-repeat="(k,theme) in themes" ng-class="{active: k == current_theme}" class="list-group-item" ng-href="/{{ k }}"><span class="badge"><span class="glyphicon glyphicon-fire" aria-hidden="true" ng-if="max_solved > 1 && theme.solved == max_solved" alt="Déjà {{ theme.solved }} challenges résolus dans ce thème"></span> <span class="glyphicon glyphicon-gift" aria-hidden="true" ng-if="theme.exercice_coeff_max > 1" alt="Des bonus existent pour au moins un challenge de ce thème"></span> <span ng-if="(my.team_id)">{{ theme.exercice_solved }}/</span>{{ theme.exercice_count }}</span>{{ theme.name }}</a>
</div>
<div class="list-group">
<a ng-repeat="(k,theme) in themes" ng-class="{active: k == current_theme}" class="list-group-item" href="/{{ k }}"><span class="badge"><span ng-show="(my.team_id)">{{ theme.exercice_solved }}/</span>{{ theme.exercice_count }}</span>{{ theme.name }}</a>
</div>
<a href="https://srs.epita.fr/">
<img src="/img/srs.png" class="center-block" alt="Epita">
</a>
<a href="https://srs.epita.fr/"><img src="/img/srs.png" alt="Laboratoire SRS Épita" style="width: 48%; max-width: 200px;"></a>
<img src="/img/rcc.png" alt="Réserve Citoyenne Cyberdéfense" style="width: 48%; max-width: 200px;">
</div>
<div class="col-sm-9" ng-view>
<noscript>
<div class="alert alert-danger">
<strong>Veuillez activer le JavaScript.</strong> Ce site requiert un navigateur interprêtant le JavaScript pour fonctionner. Veuillez l'activer ou en télécharger un supportant cette technologie.
</div>
</noscript>
</div>
</div>
@ -116,6 +123,7 @@
<script src="/js/angular-route.min.js"></script>
<script src="/js/angular-sanitize.min.js"></script>
<script src="/js/i18n/angular-locale_fr-fr.js"></script>
<script src="/js/app.js"></script>
<script src="/js/challenge.js"></script>
<script src="/js/common.js"></script>
</body>
</html>

View file

@ -1,6 +1,10 @@
angular.module("FICApp", ["ngRoute", "ngSanitize"])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when("/rules", {
controller: "HomeController",
templateUrl: "views/rules.html"
})
.when("/edit", {
controller: "MyTeamController",
templateUrl: "views/team-edit.html"
@ -9,6 +13,10 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
controller: "RankController",
templateUrl: "views/rank.html"
})
.when("/videos", {
controller: "VideosController",
templateUrl: "views/videos.html"
})
.when("/:theme", {
controller: "ExerciceController",
templateUrl: "views/theme.html"
@ -26,14 +34,12 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
});
$locationProvider.html5Mode(true);
})
.run(function($rootScope, $timeout) {
.run(function($rootScope, $interval) {
$rootScope.current_theme = 0;
$rootScope.current_exercice = 0;
$rootScope.time = {};
function updTime() {
$timeout.cancel($rootScope.cbm);
$rootScope.cbm = $timeout(updTime, 1000);
if (sessionStorage.userService) {
var time = angular.fromJson(sessionStorage.userService);
var srv_cur = (Date.now() + (time.cu * 1000 - time.he)) / 1000;
@ -64,33 +70,7 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
}
}
updTime();
});
String.prototype.capitalize = function() {
return this
.toLowerCase()
.replace(
/(^|\s)([a-z])/g,
function(m,p1,p2) { return p1+p2.toUpperCase(); }
);
}
angular.module("FICApp")
.filter("capitalize", function() {
return function(input) {
return input.capitalize();
}
})
.filter("time", function() {
return function(input) {
if (input == undefined) {
return "--";
} else if (input >= 10) {
return input;
} else {
return "0" + input;
}
}
$interval(updTime, 1000);
})
.controller("DataController", function($sce, $scope, $http, $rootScope, $timeout) {
var actMenu = function() {
@ -113,16 +93,25 @@ angular.module("FICApp")
time.he = (new Date()).getTime();
sessionStorage.userService = angular.toJson(time);
});
$http.get("/settings.json").success(function(settings) {
$scope.settings = settings;
});
$http.get("/themes.json").success(function(themes) {
$scope.themes = themes;
$scope.max_gain = 0;
$scope.max_solved = 0;
angular.forEach(themes, function(theme, key) {
this[key].exercice_count = Object.keys(theme.exercices).length;
this[key].exercice_coeff_max = 0;
this[key].gain = 0;
this[key].solved = 0;
angular.forEach(theme.exercices, function(ex, k) {
this.gain += ex.gain;
this.solved += ex.solved;
this.exercice_coeff_max = Math.max(this.exercice_coeff_max, ex.curcoeff);
}, theme);
$scope.max_gain += theme.gain;
$scope.max_solved = Math.max($scope.max_solved, theme.solved);
}, themes);
actMenu();
});
@ -140,6 +129,7 @@ angular.module("FICApp")
$http.get("/my.json").success(function(my) {
$scope.my = my;
angular.forEach($scope.my.exercices, function(exercice, eid) {
exercice.solved = exercice.solved_rank > 0;
if (exercice.video_uri) {
exercice.video_uri = $sce.trustAsResourceUrl(exercice.video_uri);
}
@ -150,7 +140,7 @@ angular.module("FICApp")
}
$rootScope.refresh();
})
.controller("ExerciceController", function($scope, $routeParams, $http, $rootScope) {
.controller("ExerciceController", function($scope, $routeParams, $http, $rootScope, $timeout) {
$rootScope.current_theme = $routeParams.theme;
if ($routeParams.exercice) {
@ -173,9 +163,38 @@ angular.module("FICApp")
}
}
$scope.hsubmit = function(hint) {
hint.submitted = true;
$http({
url: "/openhint/" + $rootScope.current_exercice,
method: "POST",
data: {
id: hint.id
}
}).success(function(data, status, header, config) {
var checkDiffHint = function() {
$http.get("/my.json").success(function(my) {
angular.forEach(my.exercices[$rootScope.current_exercice].hints, function(h,hid){
if (hint.id == h.id) {
if (hint.content != h.content) {
$rootScope.refresh();
} else {
$timeout.cancel($scope.cbh);
$scope.cbh = $timeout(checkDiffHint, 750);
}
}
});
});
};
checkDiffHint();
}).error(function(data, status, header, config) {
hint.submitted = false;
console.error(data.errmsg);
});
};
})
.controller("SubmissionController", function($scope, $http, $rootScope, $timeout) {
$scope.flags = []
$scope.flags = [];
$rootScope.sberr = "";
var waitMy = function() {
@ -183,12 +202,16 @@ angular.module("FICApp")
$timeout.cancel($scope.cbs);
$scope.cbs = $timeout(waitMy, 420);
} else {
$scope.flags = [];
angular.forEach($scope.my.exercices[$rootScope.current_exercice].keys, function(key,kid) {
this.push({
var o = {
id: kid,
name: key,
value: ""
});
};
if ($scope.my.exercices[$rootScope.current_exercice].solved_matrix != null)
o.found = $scope.my.exercices[$rootScope.current_exercice].solved_matrix[kid];
this.push(o);
}, $scope.flags);
}
}
@ -197,23 +220,10 @@ angular.module("FICApp")
$scope.ssubmit = function() {
var flgs = {}
var filled = true;
angular.forEach($scope.flags, function(flag,kid) {
flgs[flag.name] = flag.value;
filled = filled && flag.value.length > 0;
});
if (!filled) {
$rootScope.messageClass = {"text-danger": true};
$rootScope.sberr = "Tous les champs sont obligatoires.";
$timeout(function() {
if ($rootScope.sberr == "Tous les champs sont obligatoires.") {
$rootScope.sberr = "";
}
}, 2345);
return;
}
$http({
url: "/submit/" + $rootScope.current_exercice,
method: "POST",
@ -231,6 +241,8 @@ angular.module("FICApp")
$http.get("/my.json").success(function(my) {
if ($scope.my.exercices[$rootScope.current_exercice].solved_time != my.exercices[$rootScope.current_exercice].solved_time) {
$rootScope.refresh();
$scope.my = my;
waitMy();
} else {
$timeout.cancel($scope.cbd);
$scope.cbd = $timeout(checkDiff, 750);
@ -327,10 +339,16 @@ angular.module("FICApp")
$scope.reverse = !$scope.reverse;
} else {
$scope.rankOrder = fld;
$scope.reverse = false;
$scope.reverse = (fld == "score");
}
};
})
.controller("VideosController", function($scope, $rootScope) {
$rootScope.current_theme = 0;
$rootScope.current_exercice = 0;
$rootScope.title = "Vidéos de résolution";
$rootScope.authors = "";
})
.controller("HomeController", function($scope, $rootScope) {
$rootScope.current_theme = 0;
$rootScope.current_exercice = 0;

View file

@ -0,0 +1,83 @@
String.prototype.capitalize = function() {
return this
.toLowerCase()
.replace(
/(^|\s)([a-z])/g,
function(m,p1,p2) { return p1+p2.toUpperCase(); }
);
}
angular.module("FICApp")
.filter("capitalize", function() {
return function(input) {
return input.capitalize();
}
})
.filter("rankTitle", function() {
var itms = {
"rank": "Rang",
"name": "Équipe",
"score": "Score",
};
return function(input) {
if (itms[input] != undefined) {
return itms[input];
} else {
return input;
}
}
})
.filter("time", function() {
return function(input) {
if (input == undefined) {
return "--";
} else if (input >= 10) {
return input;
} else {
return "0" + input;
}
}
})
.filter("since", function() {
return function(passed) {
if (passed < 120000) {
return "Il y a " + Math.floor(passed/1000) + " secondes";
} else {
return "Il y a " + Math.floor(passed/60000) + " minutes";
}
}
})
.filter("size", function() {
var units = [
"o",
"kio",
"Mio",
"Gio",
"Tio",
"Pio",
"Eio",
"Zio",
"Yio",
]
return function(input) {
var res = input;
var unit = 0;
while (res > 1024) {
unit += 1;
res = res / 1024;
}
return (Math.round(res * 100) / 100) + " " + units[unit];
}
})
.filter("coeff", function() {
return function(input) {
if (input > 1) {
return "+" + Math.floor((input - 1) * 100) + " %"
} else if (input < 1) {
return "-" + Math.floor((1 - input) * 100) + " %"
} else {
return "";
}
}
})

5
frontend/static/js/d3.v3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -10,31 +10,6 @@ String.prototype.capitalize = function() {
}
angular.module("FICApp")
.filter("capitalize", function() {
return function(input) {
return input.capitalize();
}
})
.filter("since", function() {
return function(passed) {
if (passed < 120000) {
return "Il y a " + Math.floor(passed/1000) + " secondes";
} else {
return "Il y a " + Math.floor(passed/60000) + " minutes";
}
}
})
.filter("time", function() {
return function(input) {
if (input == undefined) {
return "--";
} else if (input >= 10) {
return input;
} else {
return "0" + input;
}
}
})
.controller("TimeController", function($scope, $rootScope, $http, $timeout) {
$scope.time = {};
var initTime = function() {
@ -85,29 +60,84 @@ angular.module("FICApp")
}
updTime();
})
.controller("DataController", function($scope, $http, $rootScope, $timeout) {
$rootScope.refresh = function() {
$timeout.cancel($scope.cbd);
$scope.cbd = $timeout($rootScope.refresh, 4200);
$http.get("/demo.json").success(function(demo) {
$scope.my = demo;
});
$http.get("/events.json").success(function(evts) {
var events = {};
.controller("EventsController", function($scope, $http, $interval) {
$scope.events = [];
var refreshEvents = function() {
$http.get("/events.json").then(function(response) {
if ($scope.lasteventsetag != undefined && $scope.lasteventsetag == response.headers().etag)
return;
$scope.lasteventsetag = response.headers().etag;
var events = response.data;
var now = new Date();
angular.forEach(evts, function(event, key) {
events["e" + event.id] = event;
event.since = now.getTime() - now.getTimezoneOffset() * 60000 - Date.parse(event.time);
var key = 0;
angular.forEach($scope.events, function(event) {
event.keep = 0;
});
angular.forEach(events, function(event) {
event.time = Date.parse(event.time);
//event.since = now.getTime() - now.getTimezoneOffset() * 60000 - event.time;
event.since = now.getTime() - event.time;
event.keep = 1;
while (key <= $scope.events.length) {
if (key >= $scope.events.length) {
$scope.events.push(event);
break;
} else if (event.id == $scope.events[key].id) {
$scope.events[key].txt = event.txt;
$scope.events[key].time = event.time;
$scope.events[key].kind = event.kind;
$scope.events[key].since = event.since;
$scope.events[key].keep = 1;
break;
} else if (event.time > $scope.events[key].time) {
$scope.events.unshift(event);
break;
} else {
key += 1;
}
}
});
angular.forEach($scope.events, function(event, i) {
if (event.keep == 0) {
$scope.events.splice(i, 1);
}
});
$scope.events = events;
});
$http.get("/public.json").success(function(scene) {
$scope.scene = scene;
}
refreshEvents()
$interval(refreshEvents, 2100);
})
.controller("DataController", function($scope, $http, $rootScope, $interval) {
var refreshScene = function() {
$http.get("/public.json").then(function(response) {
if ($scope.lastpublicetag == response.headers().etag)
return;
$scope.lastpublicetag = response.headers().etag;
$scope.scene = response.data;
});
}
var refreshData = function() {
$http.get("/my.json").then(function(response) {
if ($scope.lastmyetag == response.headers().etag)
return;
$scope.lastmyetag = response.headers().etag;
$scope.my = response.data;
});
$http.get("/stats.json").success(function(stats) {
$scope.stats = stats;
});
$http.get("/themes.json").success(function(themes) {
$http.get("/settings.json").success(function(settings) {
$scope.settings = settings;
});
$http.get("/themes.json").then(function(response) {
if ($scope.lastthemeetag == response.headers().etag)
return;
$scope.lastthemeetag = response.headers().etag;
var themes = response.data;
$scope.themes = themes;
$scope.max_gain = 0;
angular.forEach(themes, function(theme, key) {
@ -119,7 +149,13 @@ angular.module("FICApp")
$scope.max_gain += theme.gain;
}, themes);
});
$http.get("/teams.json").success(function(teams) {
$http.get("/teams.json").then(function(response) {
if ($scope.lastteametag == response.headers().etag)
return;
$scope.lastteametag = response.headers().etag;
var teams = response.data;
$scope.teams_count = Object.keys(teams).length
$scope.teams = teams;
@ -131,7 +167,15 @@ angular.module("FICApp")
}
}, $scope.rank);
});
console.log("refresh!");
}
$rootScope.refresh();
refreshData();
refreshScene();
$interval(refreshData, 4200);
$interval(refreshScene, 900);
})
.controller("TeamController", function($scope, $http, $interval) {
$scope.mystats = null;
$http.get("/api/teams/" + $scope.team.id + "/stats.json").success(function(mstats) {
$scope.mystats = mstats;
});
});

View file

@ -20,142 +20,135 @@
<base href="/">
<script src="/js/angular.min.js"></script>
</head>
<body style="overflow: hidden" ng-controller="DataController">
<body style="overflow: hidden;" class="container-fluid" ng-controller="DataController">
<div class="row" style="margin-top: 10px">
<div class="col-xs-8">
<noscript>
<div class="alert alert-danger">
<strong>Veuillez activer le JavaScript.</strong> Ce site requiert un navigateur interprêtant le JavaScript pour fonctionner. Veuillez l'activer ou en télécharger un supportant cette technologie.
</div>
</noscript>
<noscript>
<div class="alert alert-danger">
<strong>Veuillez activer le JavaScript.</strong> Ce site requiert un navigateur interprêtant le JavaScript pour fonctionner. Veuillez l'activer ou en télécharger un supportant cette technologie.
</div>
</noscript>
<div ng-repeat="(k,s) in scene" class="repeatedd-item">
<div class="navbar navbar-default" ng-controller="TimeController" style="position:relative;z-index:1;">
<div class="container">
<div class="row">
<div class="navbar-header col-sm-2">
<a href="/">
<img src="/img/fic.png" alt="Forum International de la Cybersécurité">
</a>
</div>
<div class="navbar-right col-sm-2">
<a href="http://www.epita.fr/">
<img src="/img/epita.png" alt="Epita" class="center-block">
</a>
</div>
<div id="clock" class="col-sm-7" ng-class="{expired: time.expired, end: time.end}" ng-show="time.hours === 0 || time.hours">
<span id="hours">{{ time.hours | time }}</span>
<span class="point">:</span>
<span id="min">{{ time.minutes | time }}</span>
<span class="point">:</span>
<span id="sec">{{ time.seconds | time }}</span>
</div>
<div id="clock" class="col-sm-7" ng-show="!(time.hours === 0 || time.hours)">
{{ time.start | date:"shortDate" }}
</div>
</div>
</div>
</div>
<div class="well" ng-if="s.type == 'welcome' && !s.params.hide && s.params.kind == 'init'">
<h1>Bienvenue au challenge forensic&nbsp;!</h1>
<p class="lead text-justify">
Avant de vous installer, venez récupérer votre clef USB auprès
de notre équipe. Elle contient le certificat qui vous permettra
de vous authentifier auprès de notre serveur.
</p>
<p class="lead text-justify">
Une fois connecté au réseau, contactez notre serveur sur&nbsp;:
<span style="display: block; font-size: 200%" class="text-center">
<a style="font-family: mono" href="https://fic.srs.epita.fr/"><span class="text-info">https://fic.srs.epita.fr/</span></a>
</span>
</p>
</div>
<!--div style="margin: 5px 0;">
Bandeau Twitter catchant #FIC2016
</div-->
<div class="text-justify" style="margin: 10px;">
<div class="row">
<div class="col-sm-8">
<div ng-repeat="(k,s) in scene" class="repeatedd-item">
<div class="well" ng-show="s.type == 'welcome' && !s.go">
<h1>Bienvenue au challenge forensic !</h1>
<p class="lead text-justify">
Avant de vous installer, venez récupérer votre clef USB auprès
de notre équipe. Elle contient le certificat qui vous permettra
de vous authentifier auprès de notre serveur.
</p>
<p class="lead text-justify">
Une fois connecté au réseau, vous pouvez accéder au serveur à :
<span style="display: block; font-size: 200%" class="text-center">
<a href="https://fic.srs.epita.fr/"><span class="text-info">https://fic.srs.epita.fr/</span></a>
</span>
</p>
<div class="panel panel-success" ng-if="s.type == 'welcome' && !s.params.hide && s.params.kind == 'countdown'">
<div class="panel-heading">
<h3 class="panel-title"><strong>Le {{ settings.title }} est sur le point de commencer&nbsp;!</strong></h3>
</div>
<div class="panel-body text-center" style="font-size: 450%;" ng-if="startIn">{{ startIn }}</div>
<div class="panel-body text-center" style="font-size: 450%;" ng-if="!startIn">Go, go, go&nbsp;!</div>
</div>
<div class="panel panel-success" ng-show="s.type == 'welcome' && s.go == 1 && startIn">
<div class="panel-heading">
<h3 class="panel-title"><strong>Le challenge forensic 2016 est sur le point de démarrer !</strong></h3>
</div>
<div class="panel-body text-center point" style="font-size: 350%;">{{ startIn }}</div>
<div class="well" ng-if="s.type == 'welcome' && !s.params.hide && s.params.kind == 'public'">
<h1>Bienvenue au challenge forensic&nbsp;!</h1>
<p class="lead text-justify">
Durant ce challenge, les équipes doivent remonter des scénarii
d'attaques auxquels nos systèmes d'information font faces
chaque jour : fuite de données, compromission d'un poste de
travail, exploitation de vulnérabilités d'un site web, ...
</p>
<p class="lead text-justify">
Pour valider un challenge, chaque participant va télécharger :
soit des journaux d'évènements, des extraits de trafic réseau
ou même des copies figées de la mémoire vive de machines
malveillantes, pour essayer de comprendre comment l'attaquant a
contourné la sécurité de la machine et quelles actions hostiles
ont été effectuées.
</p>
</div>
<div class="well" ng-if="s.type == 'message' && !s.params.hide">
<h1 ng-if="s.params.title"><strong>{{ s.params.title }}</strong></h1>
<p ng-if="s.params.lead" class="lead text-justify">{{ s.params.lead }}</p>
<p ng-bind-html="s.params.html" ng-if="s.params.html"></p>
<p ng-if="s.params.text">{{ s.params.text }}</p>
</div>
<div class="panel {{ s.params.kind }}" ng-if="s.type == 'panel' && !s.params.hide">
<div class="panel-heading" ng-if="s.params.title">
<h3 class="panel-title">{{ s.params.title }}</h3>
</div>
<div class="panel-body" ng-if="s.params.text">{{ s.params.text }}</div>
<div class="panel-body" ng-if="s.params.html" ng-bind-html="s.params.html"></div>
</div>
<div class="well" ng-show="s.type == 'welcome' && s.go == 2">
<h1>Bienvenue au challenge forensic !</h1>
<p class="lead text-justify">
Durant ce challenge, les équipes doivent remonter des scénarii
d'attaques auxquels nos systèmes d'information font faces
chaque jour : fuite de données, compromission d'un poste de
travail, exploitation de vulnérabilités d'un site web, ...
</p>
<p class="lead text-justify">
Pour valider un challenge, chaque participant va télécharger,
en fonction des exercices : soit des journaux d'évènements, des
extraits de trafic réseau ou même des copies figées de la
mémoire vive de machines malveillantes, pour essayer de
comprendre comment l'attaquant a contourné la sécurité de la
machine et quelles actions hostiles ont été effectuées.
</p>
</div>
<div class="well" ng-if="s.type == 'exercice' && !s.params.hide">
<p class="lead">
<strong>Challenge <em>{{ themes[my.exercices[s.params.exercice].theme_id].exercices[s.params.exercice].title }}</em> du thème {{ themes[my.exercices[s.params.exercice].theme_id].name }}</strong>
<small class="authors" ng-if="themes[my.exercices[s.params.exercice].theme_id].authors">par {{ themes[my.exercices[s.params.exercice].theme_id].authors }}</small>
</p>
<p ng-bind-html="my.exercices[s.params.exercice].statement"></p>
<ul class="list-inline">
<li>Rapporte <ng-pluralize count="themes[my.exercices[s.params.exercice].theme_id].exercices[s.params.exercice].gain" when="{'0': 'aucun point', 'one': '{} point', 'other': '{} points'}"></ng-pluralize></li>
<li><ng-pluralize count="my.exercices[s.params.exercice].files.length" when="{'0': 'Aucun fichier disponible', 'one': '{} fichier disponible', 'other': '{} fichiers disponibles'}"></ng-pluralize></li>
<li ng-if="my.exercices[s.params.exercice].hints.length"><ng-pluralize count="my.exercices[s.params.exercice].hints.length" when="{'0': 'Aucun indice disponible', 'one': '{} indice disponible', 'other': '{} indices disponibles'}"></ng-pluralize></li>
<li>Tenté par <ng-pluralize count="themes[my.exercices[s.params.exercice].theme_id].exercices[s.params.exercice].tried" when="{'0': 'aucune équipe', 'one': '{} équipe', 'other': '{} équipes'}"></ng-pluralize></li>
<li><ng-pluralize count="my.exercices[s.params.exercice].solved_number" when="{'0': 'aucun tentative', 'one': '{} tentative', 'other': '{} tentatives'}"></ng-pluralize></li>
<li>Résolu par <ng-pluralize count="themes[my.exercices[s.params.exercice].theme_id].exercices[s.params.exercice].solved" when="{'0': 'aucune équipe', 'one': '{} équipe', 'other': '{} équipes'}"></ng-pluralize></li>
</ul>
</div>
<div class="well" ng-show="s.type == 'message'">
<h1 ng-show="s.title"><strong>{{ s.title }}</strong></h1>
<p ng-show="s.lead" class="lead text-justify">{{ s.lead }}</p>
<p ng-bind-html="s.html" ng-show="s.html"></p>
<p ng-show="s.text">{{ s.text }}</p>
</div>
<div class="panel {{ s.kind }}" ng-show="s.type == 'panel'">
<div class="panel-heading" ng-show="s.title">
<h3 class="panel-title">{{ s.title }}</h3>
</div>
<div class="panel-body" ng-show="s.text">{{ s.text }}</div>
<div class="panel-body" ng-show="s.html" ng-bind-html="s.html"></div>
</div>
<div class="well" ng-show="s.type == 'exercice'">
<p class="lead">
<strong>{{ themes[my.exercices[s.id].theme_id].exercices[s.id].title }} du challenge {{ themes[my.exercices[s.id].theme_id].name }}</strong>
<small class="authors" ng-show="themes[my.exercices[s.id].theme_id].authors">par {{ themes[my.exercices[s.id].theme_id].authors }}</small>
</p>
<p ng-bind-html="my.exercices[s.id].statement"></p>
<ul class="list-inline">
<li>{{ themes[my.exercices[s.id].theme_id].exercices[s.id].gain }} points</li>
<li>{{ my.exercices[s.id].files.length }} fichiers disponibles</li>
<li>Tenté par {{ themes[my.exercices[s.id].theme_id].exercices[s.id].tried }} équipes</li>
<li>{{ my.exercices[s.id].solved_number }} tentatives</li>
<li>Résolu par {{ themes[my.exercices[s.id].theme_id].exercices[s.id].solved }} équipes</li>
</ul>
</div>
<table class="table table-bordered table-striped" ng-show="s.type == 'table'">
<div class="panel" ng-if="s.type == 'table' && !s.params.hide">
<table class="table table-bordered table-striped table-condensed">
<thead>
<tr>
<th></th>
<th class="text-center">Niveau 1</th>
<th class="text-center">Niveau 2</th>
<th class="text-center">Niveau 3</th>
<th class="text-center">Niveau 4</th>
<th class="text-center">Niveau 5</th>
<th class="frotated"></th>
<th class="rotated" ng-repeat="(tid,th) in themes" ng-if="s.params.themes.indexOf(tid-0) !== -1"><div><span>{{ th.name }}</span></div></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="th in s.themes">
<th>{{ themes[th].name }}</th>
<td ng-repeat="ex in themes[th].exercices" class="text-center" ng-class="{'text-primary': ex.solved == 0, 'text-success': ex.solved >= 1, 'text-bold': ex.solved >= 1, 'text-warning': ex.solved == 0 && ex.tried}" ng-show="ex.solved || ex.tried || $first"><span ng-show="ex.solved">{{ ex.solved }}</span><span ng-show="!ex.solved">{{ ex.tried }}</span></td>
<tbody ng-if="s.params.kind == 'levels'">
<tr ng-repeat="lvl in [1,2,3,4,5]">
<th class="text-center"><nobr>Niveau {{ lvl }}</nobr></th>
<td ng-repeat="(tid,th) in themes" class="text-center" ng-if="s.params.themes.indexOf(tid-0) !== -1">
<span ng-repeat="exercice in th.exercices" ng-if="$index == lvl-1 && (exercice.tried || lvl == 1)" ng-class="{'text-primary': exercice.solved == 0, 'text-success': exercice.solved >= 1, 'text-bold': exercice.solved >= 1, 'text-warning': exercice.solved == 0 && exercice.tried}">
<span ng-if="exercice.solved">{{ exercice.solved }}</span>
<span ng-if="!exercice.solved">{{ exercice.tried }}</span>
</span>
</td>
</tr>
</tbody>
<tbody ng-if="s.params.kind == 'teams'">
<tr ng-repeat="team in rank | orderBy: 'rank' | limitTo: s.params.limit: s.params.begin" ng-if="s.params.teams.indexOf(team.id-0) !== -1" ng-controller="TeamController">
<th class="text-center">{{ team.rank }}<sup ng-if="team.rank == 1">er</sup><sup ng-if="team.rank > 1">e</sup><br><span style="text-overflow: ellipsis; display: inline-block; width: 82px;overflow: hidden;">{{ team.name }}</span></th>
<td ng-repeat="(tid,th) in themes" class="text-center" ng-if="mystats && s.params.themes.indexOf(tid-0) !== -1">
<span ng-class="{'text-success': mystats.themes[tid].solved}">{{ mystats.themes[tid].solved }}/{{ mystats.themes[tid].total }}</span>
<span ng-class="{'text-warning': mystats.themes[tid].solved < mystats.themes[tid].tried}">({{ mystats.themes[tid].tries }})</span>
</td>
</tr>
</tbody>
<tfoot ng-if="s.params.total" ng-init="team={id:0}">
<tr ng-controller="TeamController">
<td class="text-right">
<span ng-if="s.params.kind == 'levels'">Résolus</span>
<span ng-if="s.params.kind == 'teams'">Total résolus</span><br>
Tentatives
</td>
<td ng-repeat="(tid,th) in themes" class="text-center" ng-if="mystats && s.params.themes.indexOf(tid-0) !== -1">
<strong>{{ mystats.themes[tid].solved }}</strong><br>
{{ mystats.themes[tid].tries }}
</td>
</tr>
</tfoot>
</table>
</div>
<table class="table table-bordered table-striped" ng-show="s.type == 'rank'">
<div class="panel" ng-if="s.type == 'rank' && !s.params.hide">
<table class="table table-bordered table-striped table-condensed">
<thead>
<tr>
<th class="text-right">Place</th>
@ -163,31 +156,139 @@
<th>Score</th>
</tr>
</thead>
<tbody ng-show="s.which == 'general'">
<tr ng-repeat="r in rank | orderBy: 'rank' | limitTo: 7">
<th class="text-right">{{ r.rank }}<sup ng-show="r.rank == 1">er</sup><sup ng-show="r.rank > 1">e</sup></th>
<tbody ng-if="s.params.which == 'general'">
<tr ng-repeat="r in rank | orderBy: 'rank' | limitTo: s.params.limit: s.params.begin">
<th class="text-right">{{ r.rank }}<sup ng-if="r.rank == 1">er</sup><sup ng-if="r.rank > 1">e</sup></th>
<td>{{ r.name }}</td>
<td>{{ r.score }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="col-xs-4">
<div ng-repeat="e in events" class="repeated6-item">
<div ng-controller="EventsController">
<div ng-repeat="e in events | limitTo: 7" class="repeated-item">
<div class="alert" ng-class="e.kind">
<div class="heading">{{ e.since | since }}</div>
<span ng-bind-html="e.txt"></span>
</div>
</div>
<a href="https://srs.epita.fr/">
<img src="/img/srs.png" class="center-block" alt="Epita">
</a>
<div style="box-shadow: 0px -5px 5px 5px #272b30; position: fixed; bottom: calc(14vh - 1px); right: 0; width: 33vw;" class="navbar navbar-inverse">
<div class="text-center" ng-if="time.hours === 0 || time.hours" style="margin-top: -5px;" ng-controller="TimeController">
<div id="clock" ng-class="{expired: time.expired, end: time.end}" style="font-size: 50px" ng-cloak>
<span id="hours">{{ time.hours | time }}</span>
<span class="point">:</span>
<span id="min">{{ time.minutes | time }}</span>
<span class="point">:</span>
<span id="sec">{{ time.seconds | time }}</span>
</div>
<div style="font-size: 18px; margin-top: -15px; text-shadow: 0 0 6px #446688;">
<span ng-if="!time.end && time.duration != time.remaining">Temps restant du challenge forensic</span>
<span ng-if="!time.end && time.duration == time.remaining">Le challenge forensic va bientôt commencer&nbsp;!</span>
<span ng-if="time.end">Le challenge forensic est terminé&nbsp;!</span>
</div>
</div>
<div id="clock" class="col-sm-6" ng-if="!(time.hours === 0 || time.hours)">
{{ time.start | date:"shortDate" }}
</div>
</div>
<div style="position: fixed; bottom: 0; right: 0; width: 33vw; height: 14vh" class="navbar navbar-inverse">
<div class="carousel slide" id="carousel-logos" data-ride="carousel">
<div class="carousel-inner">
<div class="item active">
<div class="carousel-caption">
<a href="http://www.epita.fr/"><img src="/img/epita.png" alt="Epita" style="width:30%"></a>
<a href="https://srs.epita.fr/"><img src="/img/srs.png" alt="Laboratoire SRS" style="width:30%"></a>
<img src="/img/rcc.png" alt="Réserve Citoyenne Cyberdéfense" style="width:30%">
</div>
</div>
<div class="item">
<div class="carousel-caption" style="padding: 0 10px">
<h2>Bienvenue au challenge forensic&nbsp;!</h2>
</div>
</div>
<div class="item">
<div class="carousel-caption">
<p class="text-justify text-bold" style="padding: 20px 16px; font-size: 111%">
Ce challenge met en scène des scénarii d'attaques auxquels
nos systèmes d'information font faces chaque jour.
</p>
</div>
</div>
<div class="item">
<div class="carousel-caption">
<p class="text-justify text-bold" style="padding: 20px 16px; font-size: 111%">
Les 42 équipes doivent, en 4 heures, retracer les attaques à la
recherche des données exfiltrées.
</p>
</div>
</div>
<div class="item">
<div class="carousel-caption row" style="padding: 12px">
<div class="col-xs-4">
<a href="https://www.epita.fr/"><img src="/img/epita.png" alt="Épita" style="height: 10vh"></a>
</div>
<p class="col-xs-8 text-bold" style="font-size: 111%">
Les challenges ont été réalisés par les étudiants de la
spécialisation SRS de l'Épita
</p>
</div>
</div>
<div class="item">
<div class="carousel-caption row" style="padding: 12px">
<div class="col-xs-4">
<img src="/img/rcc.png" alt="Réserve Citoyenne Cyberdéfense" style="max-width:100%; max-height: 11vh">
</div>
<p class="col-xs-8 text-bold" style="padding: 10px 10px 10px 0; font-size: 111%">
Avec le parrainage du réseau de la Réserve Citoyenne Cyberdéfense
</p>
</div>
</div>
<div class="item">
<div class="carousel-caption" style="padding: 3px 25px; width: 32vw;">
<table class="table table-bordered table-condensed table-striped">
<tbody>
<tr>
<td>11&nbsp;h</td>
<td>Accueil des équipes</td>
</tr>
<tr>
<td>11&nbsp;h&nbsp;30</td>
<td>Début du challenge</td>
</tr>
<tr>
<td>15&nbsp;h&nbsp;30</td>
<td>Fin du challenge</td>
</tr>
<tr>
<td>16&nbsp;h&nbsp;00</td>
<td>Remise des prix</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="item">
<div class="carousel-caption row" style="padding: 12px">
<p class="text-bold" style="padding: 5px; font-size: 105%">
Retrouvez la correction des challenges dès demain sur :
<span style="display: block; font-size: 150%" class="text-center">
<a style="font-family: mono" href="https://fic.srs.epita.fr/"><span class="text-info">https://fic.srs.epita.fr/</span></a>
</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -200,5 +301,11 @@
<script src="/js/angular-sanitize.min.js"></script>
<script src="/js/i18n/angular-locale_fr-fr.js"></script>
<script src="/js/public.js"></script>
<script src="/js/common.js"></script>
<script>
$(".carousel").carousel({
interval: 21000
});
</script>
</body>
</html>

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -1,12 +1,12 @@
<div class="well well-lg">
<h3>Bienvenue <span ng-repeat="member in my.members"><span ng-show="$last && !$first"> et </span><span ng-show="$middle">, </span>{{ member.firstname | capitalize }} {{ member.lastname | capitalize }}</span> !</h3>
<p ng-show="(my.team_id)">
<div class="well well-lg" style="text-indent: 1em">
<h3 style="text-indent: 0">Bienvenue <span ng-repeat="member in my.members"><span ng-if="$last && !$first"> et </span><span ng-if="$middle">, </span>{{ member.firstname | capitalize }} {{ member.lastname | capitalize }}</span> !</h3>
<p ng-if="(my.team_id)">
Félicitations ! vous êtes maintenant connecté à l'espace de votre
équipe <em>{{ teams[my.team_id].name }}</em>. Vous pouvez changer ce nom
dès maintenant en vous rendant sur la page de <a href="/edit">votre
équipe</a>.
</p>
<p class="text-warning" ng-show="(my.team_id && !my.members.length)">
<p class="text-warning" ng-if="(my.team_id && !my.members.length)">
Les membres de votre équipes ne sont pas encore enregistrés.
Passez voir l'équipe serveur pour corriger cela.
</p>
@ -19,63 +19,62 @@
Saurez-vous identifier les différents vecteurs de fuites de données avec
lesquels nos systèmes d'informations et nos utilisateurs font faces ?
</p>
<p>
<strong>Attention :</strong> puisqu'il s'agit de captures effectuées dans
le but de découvrir si des actes malveillants ont été commis sur différents
systèmes d'information, les contenus qui sont
téléchargeables <em>peuvent</em> contenir du contenu malveillant !
</p>
<p>
Bon courage !
</p>
</div>
<div class="alert alert-danger">
<strong>Attention :</strong> puisqu'il s'agit de captures effectuées dans le
but de découvrir si des actes malveillants ont été commis sur différents
systèmes d'information, les contenus qui sont
téléchargeables <em>peuvent</em> contenir du contenu malveillant !
</div>
<div class="panel panel-default" ng-if="(my.team_id)">
<div class="panel-heading">
<h3 class="panel-title">Progression</h3>
</div>
<div class="panel-body">
<div class="panel panel-default" ng-show="(my.team_id)">
<div class="panel-heading">
<h3 class="panel-title">Progression</h3>
</div>
<div class="panel-body">
<strong>Vous</strong>
<div class="progress progress-striped">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ my.score * 100 / max_gain }}%;">
<span class="sr-only">{{ my.score * 100 / max_gain }}% Complete</span>
</div>
<strong>Vous</strong>
<div class="progress progress-striped">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ my.score * 100 / max_gain }}%;">
<span class="sr-only">{{ my.score * 100 / max_gain }}% Complete</span>
</div>
</div>
<strong>Le temps </strong>
<strong>Le temps</strong>
<div class="progress">
<div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ 100 - time.remaining / time.duration * 100 }}%;">
<span class="sr-only">{{ time.remaining }} secondes restantes</span>
</div>
</div>
<div ng-if="rank.length && rank[0].id != my.team_id">
<strong>La meilleure équipe ({{ rank[0].name }})</strong>
<div class="progress">
<div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ 100 - time.remaining / time.duration * 100 }}%;">
<span class="sr-only">{{ time.remaining }} secondes restantes</span>
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ rank[0].score * 100 / max_gain }}%;">
<span class="sr-only">{{ rank[0].score * 100 / max_gain }}% Complete</span>
</div>
</div>
<div ng-show="rank.length && rank[0].id != my.team_id">
<strong>La meilleure équipe ({{ rank[0].name }})</strong>
<div class="progress">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ rank[0].score * 100 / max_gain }}%;">
<span class="sr-only">{{ rank[0].score * 100 / max_gain }}% Complete</span>
</div>
</div>
</div>
<div ng-show="rank[0].id == my.team_id && rank.length > 1">
<strong>La seconde équipe ({{ rank[1].name }})</strong>
<div class="progress">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ rank[1].score * 100 / max_gain }}%;">
<span class="sr-only">{{ rank[1].score * 100 / max_gain }}% Complete</span>
</div>
</div>
</div>
</div>
<div ng-if="rank[0].id == my.team_id && rank.length > 1">
<strong>La seconde équipe ({{ rank[1].name }})</strong>
<div class="progress">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="60"
aria-valuemin="0" aria-valuemax="100" style="width: {{ rank[1].score * 100 / max_gain }}%;">
<span class="sr-only">{{ rank[1].score * 100 / max_gain }}% Complete</span>
</div>
</div>
</div>
</div>
</div>
<div ng-controller="RankController" ng-show="!(my.team_id)">
<div ng-controller="RankController" ng-if="!(my.team_id)">
<ng-include src="'views/rank.html'">
</div>

View file

@ -1,18 +1,22 @@
<p><input type="search" class="form-control" placeholder="Rechercher" ng-model="query"></p>
<table class="table table-hover table-bordered table-striped">
<thead>
<tr>
<th ng-repeat="field in fields" ng-click="order(field)">
{{ field }}
<span class="glyphicon" ng-show="rankOrder === field" ng-class="{'glyphicon-up': !reverse, 'glyphicon-down': reverse}"></span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="team in rank | filter: query | orderBy:rankOrder:reverse" ng-click="show(team.id)" ng-show="team.rank">
<td ng-repeat="field in fields" ng-class="{info: my.team_id == team.id}">
{{ team[field] }}
</td>
</tr>
</tbody>
</table>
<div class="panel panel-success">
<div class="panel-body">
<input type="search" class="form-control" placeholder="Rechercher" ng-model="query">
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th ng-repeat="field in fields" ng-click="order(field)" width="33%">
{{ field | rankTitle }}
<span class="glyphicon" ng-if="rankOrder === field" ng-class="{'glyphicon-chevron-up': reverse, 'glyphicon-chevron-down': !reverse}"></span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="team in rank | filter: query | orderBy:rankOrder:reverse" ng-click="show(team.id)" ng-if="team.rank">
<td ng-repeat="field in fields" ng-class="{info: my.team_id == team.id}">
{{ team[field] }}
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,148 @@
<div class="well well-lg text-indent">
<h2>Débloquage des challenges</h2>
<p>
Au début, seul le premier challenge de chaque thème est
accessible. Les challenges de niveau supérieur sont débloqués en
validant le challenge du niveau qui les précéde.
</p>
<hr>
<h2>Le classement</h2>
<p>
Pour figurer dans le classement, il faut avoir réalisé au moins une action :
qu'elle ajoute ou retire des points.
</p>
<p>
En cas d'égalité au score, les équipes sont départagées selon leur
ordre d'arrivée à ce score.
</p>
<hr>
<h2>Calcul des points</h2>
<p>
Pour gagner des points, vous devez résoudre les challenges qui vous sont
proposés. Plus l'exercice est compliqué, plus il rapporte de points.
</p>
<div class="row">
<div class="col-sm-offset-3 col-sm-6">
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Niveau de l'exercice</th>
<th>Points rapportés</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>5 points</td>
</tr>
<tr>
<td>1</td>
<td>11 points</td>
</tr>
<tr>
<td>2</td>
<td>23 points</td>
</tr>
<tr>
<td>3</td>
<td>47 points</td>
</tr>
<tr>
<td>4</td>
<td>95 points</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3>Coût des indices</h3>
<p>
Pour vous aider, certains exercices vous proposent un ou
plusieurs <strong>indices</strong>. Ces indices vous font perdre des
points, la valeur de points perdue est indiquée pour chaque indice.
</p>
<h3>Coût de soumission</h3>
<p>
Vous disposez de 9 tentatives pour trouver la/les solutions d'un
challenge. Au delà, chaque tentative vous fait perdre une petite quantité
de points comme suit :
</p>
<div class="row">
<div class="col-sm-offset-2 col-sm-8">
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Nombre de soumissions</th>
<th>Coût par tentative</th>
</tr>
</thead>
<tbody>
<tr>
<td>0 à 9</td>
<td>0 point</td>
</tr>
<tr>
<td>10 à 19</td>
<td><ng-pluralize count="settings.submissionCostBase" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize></td>
</tr>
<tr>
<td>20 à 29</td>
<td><ng-pluralize count="settings.submissionCostBase * 2" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize></td>
</tr>
<tr>
<td>30 à 39</td>
<td><ng-pluralize count="settings.submissionCostBase * 3" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize></td>
</tr>
<tr>
<td>40 à 49</td>
<td><ng-pluralize count="settings.submissionCostBase * 4" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize></td>
</tr>
<tr>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>
Par exemple :
</p>
<ul>
<li>À 10 soumissions, vous aurez perdu <ng-pluralize count="settings.submissionCostBase" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize>.</li>
<li>À 15 soumissions, vous aurez perdu en tout <ng-pluralize count="settings.submissionCostBase * 5" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> : <samp>{{ settings.submissionCostBase }} * 5</samp>.</li>
<li>25 soumissions vous coûteront en tout <ng-pluralize count="settings.submissionCostBase * 20" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> : <samp>{{ settings.submissionCostBase }} * 10 + {{ settings.submissionCostBase * 2}} * 5</samp>.</li>
<li>50 soumissions vous coûteront en tout <ng-pluralize count="settings.submissionCostBase * 105" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> : <samp>{{ settings.submissionCostBase }} * 10 + {{ settings.submissionCostBase * 2 }} * 10 + {{ settings.submissionCostBase * 3 }} * 10 + {{ settings.submissionCostBase * 4 }} * 10 + {{ settings.submissionCostBase * 5 }}</samp>.</li>
</ul>
<p>
La dernière soumission (lorsque tous les flags sont bons) est comptabilisée
parmi ce nombre de tentatives.
</p>
<h3>Bonus</h3>
<p>
Plusieurs bonus peuvent s'appliquer en même temps, dans ce cas, le calcul
du bonus est toujours effectué à partir du nombre de points initials du
challenge.
</p>
<h4>Prem's</h4>
<p>
Un bonus de +{{ settings.firstBlood * 100 }}&nbsp;% est attribué à la première équipe qui résout un challenge.
</p>
<h4>Bonus temporaires <small><span class="glyphicon glyphicon-gift" aria-hidden="true" alt="Des
bonus existent pour au moins un challenge de ce thème"></span></small></h4>
<p>
Au cours du challenge, afin de booster les équipes ou certains challenges,
un bonus peut-être attribué si une soumission valide est envoyée durant la
période d'activité du bonus. Restez à l'écoute et observez les challenges
portant cette icône : <span class="glyphicon glyphicon-gift"
aria-hidden="true" alt="Des bonus existent pour au moins un challenge de ce
thème"></span>
</p>
</div>

View file

@ -1,14 +1,14 @@
<div class="panel panel-default">
<div class="panel-heading">Votre équipe est composée de :</div>
<div class="panel-body" ng-show="!my.members.length">
<div class="panel-body" ng-if="!my.members.length">
Passez voir l'équipe serveur pour compléter ces informations.
</div>
<ul class="list-group" ng-show="my.members.length">
<ul class="list-group" ng-if="my.members.length">
<li class="list-group-item" ng-repeat="member in my.members">
{{ member.firstname | capitalize }}
<span style="font-style: italic" ng-show="{{ member.nickname }}">{{ member.nickname }}</span>
<span style="font-style: italic" ng-if="{{ member.nickname }}">{{ member.nickname }}</span>
<span style="font-variant: small-caps;">{{ member.lastname | capitalize }}</span>
<span ng-show="member.company">- {{ member.company}}</span>
<span ng-if="member.company">- {{ member.company}}</span>
</li>
</ul>
</div>
@ -16,7 +16,7 @@
<div class="panel panel-info">
<div class="panel-heading">Changer de nom d'équipe</div>
<div class="panel-body">
<p ng-class="messageClass" ng-show="message || sberr"><strong ng-show="!sberr">Votre demande a bien été envoyée !</strong><strong ng-show="sberr">{{ sberr }}</strong> {{ message }}</p>
<p ng-class="messageClass" ng-if="message || sberr"><strong ng-if="!sberr">Votre demande a bien été envoyée !</strong><strong ng-if="sberr">{{ sberr }}</strong> {{ message }}</p>
<form class="form-horizontal" ng-submit="tsubmit()">
<div class="form-group">

View file

@ -1,88 +1,94 @@
<ul class="nav nav-tabs nav-justified">
<li ng-repeat="(k,exercice) in themes[current_theme].exercices" ng-class="{active: k == current_exercice, disabled: !my.exercices[k]}"><a ng-show="(!my.exercices[k])">{{ exercice.title }}</a><a href="/{{ current_theme }}/{{ k }}" ng-show="(my.exercices[k])">{{ exercice.title }} <span ng-show="(my.team_id && my.exercices[k].solved)" class="badge">{{ exercice.gain }}</span></a></li>
<li ng-repeat="(k,exercice) in themes[current_theme].exercices" ng-class="{active: k == current_exercice, disabled: !my.exercices[k]}"><a ng-if="(!my.exercices[k])">{{ exercice.title }}</a><a ng-href="/{{ current_theme }}/{{ k }}" ng-if="(my.exercices[k])">{{ exercice.title }} <span class="glyphicon glyphicon-gift" aria-hidden="true" ng-if="themes[current_theme].exercices[k].curcoeff > 1.0"></span> <span class="glyphicon glyphicon-ok" aria-hidden="true" ng-if="(my.team_id && my.exercices[k].solved)"></span></a></li>
</ul>
<div class="alert alert-warning" style="margin-top:15px;" ng-show="!(my.exercices[current_exercice])">
<div class="alert alert-warning" style="margin-top:15px;" ng-if="!(my.exercices[current_exercice])">
Vous n'avez pas encore accès à cet exercice.
</div>
<div style="margin-top: 15px" class="well well-lg" ng-show="(my.exercices[current_exercice])">
<div style="margin-top: 15px" class="well well-lg" ng-if="(my.exercices[current_exercice])">
<p ng-bind-html="my.exercices[current_exercice].statement"></p>
<blockquote ng-show="(my.exercices[current_exercice].hint)" ng-bind-html="my.exercices[current_exercice].hint"></blockquote>
<hr ng-show="!(my.exercices[current_exercice].hint)">
<ul>
<li><strong>Gain :</strong> {{ themes[current_theme].exercices[current_exercice].gain }} points</li>
<li><strong>Résolu par :</strong> {{ themes[current_theme].exercices[current_exercice].solved }} équipes</li>
<li><strong>Gain :</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].gain" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> <em ng-if="themes[current_theme].exercices[current_exercice].solved < 1">{{ 1 + settings.firstBlood | coeff }} prem's</em> <em ng-if="themes[current_theme].exercices[current_exercice].curcoeff != 1.0">{{ themes[current_theme].exercices[current_exercice].curcoeff | coeff }} bonus</em></li>
<li><strong>Résolu par :</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].solved" when="{'0': 'aucune équipe', 'one': '{} équipe', 'other': '{} équipes'}"></ng-pluralize>.</li>
</ul>
</div>
<div class="panel panel-info" ng-show="(my.exercices[current_exercice] && my.exercices[current_exercice].files.length)">
<div class="panel panel-default" ng-if="(my.exercices[current_exercice] && my.exercices[current_exercice].files.length)">
<div class="panel-heading">
<div class="panel-title">Téléchargements</div>
<div class="panel-title"><span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span> Téléchargements</div>
</div>
<div class="list-group">
<a ng-href="{{ file.path }}" target="_self" class="list-group-item" ng-repeat="file in my.exercices[current_exercice].files">
<h1 class="pull-left" style="margin: 7px 7px 5px -5px"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></h1>
<h4 class="list-group-item-heading"><strong><samp>{{ file.name }}</samp></strong></h4>
<p class="list-group-item-text">Taille : <span title="{{ file.size }} octets">{{ file.size | size }}</span> &ndash; SHA-1 : <samp>{{ file.checksum }}</samp></p>
</a>
</div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th></th>
<th>Nom</th>
<th>Taille</th>
<th>SHA-1</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="file in my.exercices[current_exercice].files">
<td><a href="{{ file.path }}" target="_self"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></a></td>
<td>{{ file.name }}</td>
<td>{{ file.size }}</td>
<td><code>{{ file.checksum }}</code></td>
</tr>
</tbody>
</table>
</div>
<div class="panel panel-danger" ng-show="my.team_id && my.exercices[current_exercice] && !(my.exercices[current_exercice].solved)">
<div class="panel panel-info" ng-if="(my.exercices[current_exercice] && my.exercices[current_exercice].hints.length)">
<div class="panel-heading">
<div class="panel-title">Soumettre une solution</div>
<div class="panel-title"><span class="glyphicon glyphicon-lamp" aria-hidden="true"></span> Indices</div>
</div>
<ul class="list-group" ng-show="(my.exercices[current_exercice].solved_number || my.exercices[current_exercice].submitted || sberr)">
<li class="list-group-item text-warning" ng-show="my.exercices[current_exercice].solved_number">{{ my.exercices[current_exercice].solved_number }} tentative(s) effectuée(s). Dernière solution envoyée à {{ my.exercices[current_exercice].solved_time | date:"fullDate" }}.</li>
<li class="list-group-item" ng-class="messageClass" ng-show="my.exercices[current_exercice].submitted || sberr"><strong ng-show="!sberr">Votre solution a bien été envoyée !</strong><strong ng-show="sberr">{{ sberr }}</strong> {{ message }}</li>
<div class="list-group">
<a target="_self" class="list-group-item" ng-repeat="hint in my.exercices[current_exercice].hints" ng-href="{{ hint.file }}">
<button ng-click="hsubmit(hint)" class="pull-right btn btn-info" ng-if="!(hint.content || hint.file)" ng-class="{disabled: hint.submitted}"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Débloquer</button>
<h1 class="pull-left" style="margin: 5px 7px 5px -5px" ng-if="hint.file"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></h1>
<h4 class="list-group-item-heading">{{ hint.title }}</h4>
<p class="list-group-item-text" ng-if="hint.content" ng-bind-html="hint.content"></p>
<p class="list-group-item-text" ng-if="hint.file">Cliquez ici pour télécharger l'indice.</p>
<p class="list-group-item-text" ng-if="!(hint.content || hint.file)">Débloquer cet indice vous coûtera <ng-pluralize count="hint.cost" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize>.</p>
</a>
</div>
</div>
<div class="panel panel-danger" ng-if="my.team_id && my.exercices[current_exercice] && !(my.exercices[current_exercice].solved)">
<div class="panel-heading">
<div class="panel-title"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> Soumettre une solution</div>
</div>
<ul class="list-group" ng-if="(my.exercices[current_exercice].tries || my.exercices[current_exercice].submitted || sberr)">
<li class="list-group-item text-warning" ng-if="my.exercices[current_exercice].tries"><ng-pluralize count="my.exercices[current_exercice].tries" when="{'one': '{} tentative effectuée', 'other': '{} tentatives effectuées'}"></ng-pluralize>. Dernière solution envoyée à {{ my.exercices[current_exercice].solved_time | date:"mediumTime" }}.</li>
<li class="list-group-item" ng-class="messageClass" ng-if="my.exercices[current_exercice].submitted || sberr"><strong ng-if="!sberr">Votre solution a bien été envoyée !</strong><strong ng-if="sberr">{{ sberr }}</strong> {{ message }}</li>
</ul>
<div class="panel-body" ng-show="!my.exercices[current_exercice].submitted || sberr">
<div class="panel-body" ng-if="!my.exercices[current_exercice].submitted || sberr">
<form ng-controller="SubmissionController" ng-submit="ssubmit()">
<div class="form-group" ng-repeat="key in flags">
<div class="form-group" ng-repeat="key in flags" ng-class="{'has-success': key.found, 'has-feedback': key.found}">
<label for="sol_{{ key.id }}">{{ key.name }} :</label>
<input type="text" class="form-control" id="sol_{{ key.id }}" name="sol_{{ index }}" ng-model="key.value">
<input type="text" class="form-control" id="sol_{{ key.id }}" name="sol_{{ key.id }}" ng-model="key.value" ng-disabled="key.found">
<span class="glyphicon glyphicon-ok form-control-feedback" aria-hidden="true" ng-if="key.found"></span>
</div>
<div class="form-group text-right">
<button type="submit" class="btn btn-warning" id="sbmt">Soumettre</button>
<button type="submit" class="btn btn-danger" id="sbmt">Soumettre</button>
</div>
</form>
</div>
</div>
<div class="panel panel-success" ng-show="(my.team_id && my.exercices[current_exercice].solved)">
<div class="panel panel-success" ng-if="(my.team_id && my.exercices[current_exercice].solved)">
<div class="panel-heading">
<div class="panel-title">Challenge réussi !</div>
<div class="panel-title"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> Challenge réussi !</div>
</div>
<div class="panel-body">
Vous êtes la {{ my.exercices[current_exercice].solved_number }}<sup>e</sup> équipe à avoir résolu ce challenge à {{ my.exercices[current_exercice].solved_time | date:"fullDate" }}. Vous avez marqué {{ themes[current_theme].exercices[current_exercice].gain }} points !
Vous êtes la {{ my.exercices[current_exercice].solved_rank }}<sup><ng-pluralize count="my.exercices[current_exercice].solved_rank" when="{'one': 're', 'other': 'e'}"></ng-pluralize></sup> équipe à avoir résolu ce challenge à {{ my.exercices[current_exercice].solved_time | date:"mediumTime" }}. Vous avez marqué <ng-pluralize count="my.exercices[current_exercice].gain" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> !
</div>
</div>
<div class="panel panel-success" ng-show="(!my.team_id && my.exercices[current_exercice].keys)">
<div class="panel panel-success" ng-if="(!my.team_id && my.exercices[current_exercice].keys)">
<div class="panel-heading">
<div class="panel-title">Solution du challenge</div>
<div class="panel-title"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> Solution du challenge</div>
</div>
<div class="panel-body">
<p>
Vérifiez les clefs que vous trouvez en comparant leur SHA-512 :
</p>
<dl class="dl-horizontal" ng-repeat="key in my.exercices[current_exercice].keys">
<dt>{{ key.slice(128) }}</dt>
<dd class="samp"><code>{{ key.slice(0, 128) }}</code></dd>
<dt title="{{ key.slice(128) }}">{{ key.slice(128) }}</dt>
<dd class="samp"><samp>{{ key.slice(0, 128) }}</samp></dd>
</dl>
<iframe type="text/html" ng-show="my.exercices[current_exercice].video_uri" ng-src="{{ my.exercices[current_exercice].video_uri }}" frameborder="0" style="width: 100%; height: 35vw">
Regardez la vidéo de résolution de cet exercice : <a ng-href="{{ my.exercices[current_exercice].video_uri }}">{{ my.exercices[current_exercice].video_uri }}</a>.
</iframe>
<div class="embed-responsive">
<iframe type="text/html" ng-if="my.exercices[current_exercice].video_uri" ng-src="{{ my.exercices[current_exercice].video_uri }}" class="embed-responsive-item">
Regardez la vidéo de résolution de cet exercice : <a ng-href="{{ my.exercices[current_exercice].video_uri }}">{{ my.exercices[current_exercice].video_uri }}</a>.
</iframe>
</div>
</div>
</div>

View file

@ -0,0 +1,10 @@
<div class="list-group">
<ng-repeat ng-repeat="theme in themes">
<h4 class="list-group-item">
{{ theme.name }}
</h4>
<ng-repeat ng-repeat="(eid,exercice) in theme.exercices">
<a class="list-group-item" href="{{ my.exercices[eid].video_uri }}">{{ exercice.title }}</a>
</ng-repeat>
</ng-repeat>
</div>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html ng-app="FICApp">
<head>
<meta charset="utf-8">
<title>Challenge Forensic</title>
@ -18,22 +18,22 @@
<meta name="author" content="EPITA Laboratoire SRS">
<meta name="robots" content="all">
</head>
<body>
<body class="beautiful">
<div class="navbar navbar-inverse">
<div class="container">
<div class="row">
<div class="navbar-header col-sm-3">
<div class="navbar-header col-sm-2 text-left">
<a href="/">
<img src="/img/fic.png" alt="Forum International de la Cybersécurité" class="center-block">
<img src="/img/fic.png" alt="Forum International de la Cybersécurité">
</a>
</div>
<div class="navbar-right col-sm-2">
<div class="navbar-right col-sm-2 text-right">
<a href="http://www.epita.fr/">
<img src="/img/epita.png" alt="Epita" class="center-block">
<img src="/img/epita.png" alt="Epita">
</a>
</div>
<div id="clock" class="col-sm-7">
<div id="clock" class="col-sm-8 text-center">
Challenge forensic
</div>
</div>
@ -41,28 +41,129 @@
</div>
<div class="container" style="margin-top:20px;">
<div class="jumbotron text-justify">
<h1>Bienvenue !</h1>
<div class="jumbotron text-justify" style="text-indent: 1.5em">
<h1 style="text-indent: 0">Bienvenue !</h1>
<p style="text-indent: 0">
<strong>
Vous n'êtes pas encore connecté en tant qu'équipe sur notre serveur.
</strong>
</p><hr>
<p>
Vous n'êtes pas encore connecté en tant qu'équipe sur notre serveur.
Bienvenue dans cette première épreuve du challenge forensic&nbsp;!
Votre première activité consiste à accéder au site dédié à cet
événement ; ce guide est là pour vous y aider.
</p>
<br>
<p>
Après avoir suivi le guide présent sur la clef USB que nous vous
avons remis et ajouté votre certificat dans votre système ou votre
navigateur, ce dernier devrait vous afficher une boîte de dialogue
vous demandant de choisir parmi les certificats installés sur votre
machine.
<strong>Important&nbsp;:</strong> La clef USB qui vous a été donnée
contient des fichiers permettant votre authentification auprès de
nos serveurs. Ne la laissez pas sans surveillance !
</p>
<br>
<h2>Installation du certificat client</h2>
<p>
Si vous avez accédé à cette page avant d'avoir ajouté le certificat,
il peut être nécessaire de relancer votre navigateur, afin qu'il
démarre une nouvelle session.
Le certificat <em>client</em> est envoyé à notre serveur pour vous
identifier et vous authentifier. Votre certificat et votre clef
privée sont contenu sur la clef USB que nous vous avons donné, dans
un fichier <samp>.p12</samp>, protégé avec le mot de passe qui vous a
été fourni sur papier.
</p>
<p>
Choisissez la procédure correspondant à votre navigateur ou système
d'exploitation :
</p>
<ol>
<li><a href="#cert-client-mozilla-firefox">Mozilla Firefox</a></li>
<li><a href="#cert-client-chromium">Chromium/Google Chrome</a></li>
<ol>
<li><a href="#cert-client-chromium-windows">Sous Microsoft Windows</a></li>
<li><a href="#cert-client-chromium-macos">Sous Mac OS</a></li>
<li><a href="#cert-client-gnu-linux">Sous GNU/Linux, FreeBSD ou OpenBSD</a></li>
</ol>
<li><a href="#cert-client-ie">Internet Explorer/Edge</a></li>
<li><a href="#cert-client-safari">Safari</a></li>
</ol>
<br>
<h2 id="cert-client-mozilla-firefox">Mozilla Firefox</h2>
<ol>
<li>Ouvrez la fenêtre des préférences du navigateur.</li>
<li>Choisissez la catégorie <strong>Avancé</strong>.</li>
<li>Sélectionnez l'onglet <strong>Certificats</strong>.</li>
<li>Cliquez sur <strong>Afficher les certificats</strong>.</li>
<li>Sélectionnez l'onglet <strong>Vos certificats</strong>.</li>
<li>Cliquez sur <strong>Importer…</strong> et sélectionnez votre certificat client.</li>
</ol>
<br>
<h2 id="cert-client-chromium">Chromium/Google Chrome</h2>
<h3 id="cert-client-chromium-windows">Sous Microsoft Windows</h3>
<p>
Le navigateur utilise les paramètres du système ; suivez les
instructions concernant <a href="#cert-client-ie">Internet
Explorer</a>.
</p>
<h3 id="cert-client-chromium-macos">Sous Mac OS</h3>
<ol>
<li>Ouvrez le menu des préférences du navigateur.</li>
<li>Cliquez sur <strong>Afficher les paramètres avancés</strong>.</li>
<li>Dans la section <strong>HTTPS/SSL</strong>, cliquez sur <strong>Gérer les certificats</strong>. Le trousseau d'accès se lance.</li>
<li>Dans le menu <strong>Fichier</strong>, sélectionnez <strong>Importer des éléments…</strong> et sélectionnez votre certificat client.</li>
<li>Choisissez un trousseau.</li>
</ol>
<h3 id="cert-client-chromium-gnu-linux">Sous GNU/Linux, FreeBSD ou OpenBSD</h3>
<ol>
<li>Ouvrez le menu des préférences du navigateur.</li>
<li>Cliquez sur <strong>Afficher les paramètres avancés</strong>.</li>
<li>Dans la section <strong>HTTPS/SSL</strong>, cliquez sur <strong>Gérer les certificats</strong>.</li>
<li>Sélectionnez l'onglet <strong>Vos certificats</strong>.</li>
<li>Cliquez sur <strong>Importer…</strong> et sélectionnez votre certificat client.</li>
</ol>
<br>
<h2 id="cert-client-ie">Internet Explorer</h2>
<ol>
<li>Double-cliquez sur le fichier <samp>.p12</samp> présent sur votre clef USB. L'<em>assistant d'importation du certificat</em> apparaît.</li>
<li>Cliquez sur <strong>Suivant</strong>.</li>
<li>Cliquez sur <strong>Suivant</strong>.</li>
<li>Entrez le mot de passe fourni sur papier puis cliquez sur <strong>Suivant</strong>.</li>
<li>Cliquez sur <strong>Suivant</strong> (le certificat sera automatiquement placé dans le magasin <em>Personnel</em>).</li>
<li>Cliquez sur <strong>Terminer</strong>.</li>
</ol>
<p>
Selon votre version de Windows, votre système peut ensuite vous
demander de définir un mot de passe pour protéger ce certificat.
</p>
<p>
<strong>Microsoft Internet Explorer&nbsp;:</strong> Aucune version
de <em>Microsoft Internet Explorer</em> (nom d'« Internet Explorer »
jusqu'à sa version 9 comprise) n'est supportée par notre serveur.
</p>
<br>
<h2 id="cert-client-safari">Safari</h2>
<ol>
<li><strong>Double-cliquez</strong> sur le fichier <samp>.p12</samp> présent sur votre clef USB.</li>
<li>Entrez le mot de passe fourni sur papier puis cliquez sur <strong>Suivant</strong>.</li>
</ol>
<hr>
<p>
Si malgré tout, vous n'arrivez pas à accéder à l'espace de votre
équipe ou si votre clef USB est illisible, n'hésitez pas à nous
solliciter !
</p>
<p>
Bon courage&nbsp;!
</p>
</div>
</div>

View file

@ -2,24 +2,19 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
fronttime "srs.epita.fr/fic-server/frontend/time"
)
type SubmissionHandler struct {
ChallengeEnd time.Time
DenyChName bool
AllowRegistration bool
}
type SubmissionHandler struct {}
func (s SubmissionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
log.Printf("Handling %s submission request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "application/json")
@ -35,113 +30,25 @@ func (s SubmissionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Extract URL arguments
var sURL = strings.Split(r.URL.Path, "/")
if len(sURL) == 2 && s.AllowRegistration && sURL[1] == "registration" {
if _, err := os.Stat(path.Join(SubmissionDir, "_registration")); os.IsNotExist(err) {
log.Println("Creating _registration directory")
if err := os.MkdirAll(path.Join(SubmissionDir, "_registration"), 0777); err != nil {
log.Println("Unable to create _registration directory: ", err)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError)
return
}
}
if f, err := ioutil.TempFile(path.Join(SubmissionDir, "_registration"), ""); err != nil {
log.Println("Unable to open registration file:", err)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds..\"}", http.StatusInternalServerError)
} else {
// Read request body
var body []byte
if r.ContentLength > 0 {
tmp := make([]byte, 1024)
for {
n, err := r.Body.Read(tmp)
for j := 0; j < n; j++ {
body = append(body, tmp[j])
}
if err != nil || n <= 0 {
break
}
}
}
f.Write(body)
f.Close()
}
http.Error(w, "{\"errmsg\":\"Demande d'enregistrement acceptée\"}", http.StatusAccepted)
return
} else if len(sURL) != 3 {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
if len(sURL) != 2 {
http.Error(w, "{\"errmsg\":\"Arguments manquants.\"}", http.StatusBadRequest)
return
}
team := sURL[0]
if time.Now().Sub(s.ChallengeEnd) > 0 {
if time.Now().Sub(fronttime.ChallengeEnd) > 0 {
http.Error(w, "{\"errmsg\":\"Vous ne pouvez plus soumettre, le challenge est terminé.\"}", http.StatusForbidden)
return
}
// Parse arguments
if _, err := os.Stat(path.Join(TeamsDir, sURL[1])); os.IsNotExist(err) || len(sURL[1]) < 1 || sURL[1][0] == '_' {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return
} else if pex, err := strconv.Atoi(sURL[2]); (s.DenyChName || sURL[2] != "name") && err != nil {
if pex, err := strconv.Atoi(sURL[1]); err != nil {
http.Error(w, "{\"errmsg\":\"Requête invalide.\"}", http.StatusBadRequest)
return
} else {
team := sURL[1]
exercice := fmt.Sprintf("%d", pex)
var exercice string
if sURL[2] == "name" {
exercice = sURL[2]
} else {
exercice = fmt.Sprintf("%d", pex)
if saveTeamFile(TeamsDir, team, exercice, w, r) {
http.Error(w, "{\"errmsg\":\"Son traitement est en cours...\"}", http.StatusAccepted)
}
if len(exercice) <= 0 {
log.Println("EMPTY $EXERCICE RECEIVED:", exercice)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError)
return
}
if _, err := os.Stat(path.Join(SubmissionDir, team)); os.IsNotExist(err) {
log.Println("Creating submission directory for", team)
if err := os.MkdirAll(path.Join(SubmissionDir, team), 0777); err != nil {
log.Println("Unable to create submission directory: ", err)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError)
return
}
}
// Previous submission not treated
if _, err := os.Stat(path.Join(SubmissionDir, team, exercice)); !os.IsNotExist(err) {
http.Error(w, "{\"errmsg\":\"Du calme ! une requête est déjà en cours de traitement.\"}", http.StatusPaymentRequired)
return
}
// Read request body
var body []byte
if r.ContentLength > 0 {
tmp := make([]byte, 1024)
for {
n, err := r.Body.Read(tmp)
for j := 0; j < n; j++ {
body = append(body, tmp[j])
}
if err != nil || n <= 0 {
break
}
}
}
// Store content in file
if file, err := os.Create(path.Join(SubmissionDir, team, exercice)); err != nil {
log.Println("Unable to open exercice file:", err)
http.Error(w, "{\"errmsg\":\"Internal server error. Please retry in few seconds.\"}", http.StatusInternalServerError)
return
} else {
file.Write(body)
file.Close()
}
http.Error(w, "{\"errmsg\":\"Son traitement est en cours...\"}", http.StatusAccepted)
}
}

View file

@ -1,4 +1,4 @@
package main
package time
import (
"encoding/json"
@ -8,10 +8,10 @@ import (
"time"
)
type TimeHandler struct {
StartTime time.Time
Duration time.Duration
}
var ChallengeStart time.Time
var ChallengeEnd time.Time = time.Unix(0, 0)
type TimeHandler struct {}
type timeObject struct {
Started int64 `json:"st"`
@ -24,7 +24,7 @@ func (t TimeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if j, err := json.Marshal(timeObject{t.StartTime.Unix(), time.Now().Unix(), int(t.Duration.Seconds())}); err != nil {
if j, err := json.Marshal(timeObject{ChallengeStart.Unix(), time.Now().Unix(), int(ChallengeEnd.Sub(ChallengeStart).Seconds())}); err != nil {
http.Error(w, fmt.Sprintf("{\"errmsg\":\"%q\"}", err), http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)

1
htdocs-admin Symbolic link
View file

@ -0,0 +1 @@
admin/static/

1
htdocs-frontend Symbolic link
View file

@ -0,0 +1 @@
frontend/static/

View file

@ -4,33 +4,32 @@ import (
"os/exec"
)
func GenerateCA() string {
func convOutput(in []byte, err error) (string, error) {
return string(in), err
}
func GenerateCA() (string, error) {
// Call the script and return its standard and error output
cmd := exec.Command("./CA.sh", "-newca")
cmd := exec.Command("/bin/bash", "./CA.sh", "-newca")
if output, err := cmd.CombinedOutput(); err != nil {
return string(output) + err.Error()
} else {
return string(output)
}
return convOutput(cmd.CombinedOutput())
}
func (t Team) GenerateCert() string {
cmd := exec.Command("./CA.sh", "-newclient", t.Name)
func GenerateCRL() (string, error) {
cmd := exec.Command("/bin/bash", "./CA.sh", "-gencrl")
if output, err := cmd.CombinedOutput(); err != nil {
return string(output) + err.Error()
} else {
return string(output)
}
return convOutput(cmd.CombinedOutput())
}
func (t Team) RevokeCert() string {
cmd := exec.Command("./CA.sh", "-revoke", t.Name)
func (t Team) GenerateCert() (string, error) {
cmd := exec.Command("/bin/bash", "./CA.sh", "-newclient", t.InitialName)
if output, err := cmd.CombinedOutput(); err != nil {
return string(output) + err.Error()
} else {
return string(output)
}
return convOutput(cmd.CombinedOutput())
}
func (t Team) RevokeCert() (string, error) {
cmd := exec.Command("/bin/bash", "./CA.sh", "-revoke", t.InitialName)
return convOutput(cmd.CombinedOutput())
}

View file

@ -64,9 +64,9 @@ CREATE TABLE IF NOT EXISTS exercices(
id_theme INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
statement TEXT NOT NULL,
hint TEXT NOT NULL,
depend INTEGER,
gain INTEGER NOT NULL,
coefficient_cur FLOAT NOT NULL DEFAULT 1.0,
video_uri VARCHAR(255) NOT NULL,
FOREIGN KEY(id_theme) REFERENCES themes(id_theme),
FOREIGN KEY(depend) REFERENCES exercices(id_exercice)
@ -85,6 +85,18 @@ CREATE TABLE IF NOT EXISTS exercice_files(
size INTEGER NOT NULL,
FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice)
);
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS exercice_hints(
id_hint INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_exercice INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
cost INTEGER NOT NULL,
FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice)
);
`); err != nil {
return err
}
@ -96,6 +108,17 @@ CREATE TABLE IF NOT EXISTS exercice_keys(
value BINARY(64) NOT NULL,
FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice)
);
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS key_found(
id_key INTEGER NOT NULL,
id_team INTEGER NOT NULL,
time TIMESTAMP NOT NULL,
FOREIGN KEY(id_key) REFERENCES exercice_keys(id_key),
FOREIGN KEY(id_team) REFERENCES teams(id_team)
);
`); err != nil {
return err
}
@ -104,6 +127,8 @@ CREATE TABLE IF NOT EXISTS exercice_solved(
id_exercice INTEGER NOT NULL,
id_team INTEGER NOT NULL,
time TIMESTAMP NOT NULL,
coefficient FLOAT NOT NULL DEFAULT 1.0,
CONSTRAINT uc_solved UNIQUE (id_exercice,id_team),
FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice),
FOREIGN KEY(id_team) REFERENCES teams(id_team)
);
@ -121,6 +146,21 @@ CREATE TABLE IF NOT EXISTS exercice_tries(
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS team_hints(
id_team INTEGER,
id_hint INTEGER,
time TIMESTAMP NOT NULL,
CONSTRAINT uc_displayed UNIQUE (id_team,id_hint),
FOREIGN KEY(id_hint) REFERENCES exercice_hints(id_hint),
FOREIGN KEY(id_team) REFERENCES teams(id_team)
);
`); err != nil {
return err
}
if _, err := db.Exec("SET FOREIGN_KEY_CHECKS = 1;"); err != nil {
return err
}
return nil
}

View file

@ -11,7 +11,7 @@ type Event struct {
Time time.Time `json:"time"`
}
func GetEvents() ([]Event, error) {
func GetLastEvents() ([]Event, error) {
if rows, err := DBQuery("SELECT id_event, txt, kind, time FROM events ORDER BY time DESC LIMIT 6"); err != nil {
return nil, err
} else {
@ -33,6 +33,37 @@ func GetEvents() ([]Event, error) {
}
}
func GetEvents() ([]Event, error) {
if rows, err := DBQuery("SELECT id_event, txt, kind, time FROM events ORDER BY time DESC"); err != nil {
return nil, err
} else {
defer rows.Close()
var events = make([]Event, 0)
for rows.Next() {
var e Event
if err := rows.Scan(&e.Id, &e.Text, &e.Kind, &e.Time); err != nil {
return nil, err
}
events = append(events, e)
}
if err := rows.Err(); err != nil {
return nil, err
}
return events, nil
}
}
func GetEvent(id int) (Event, error) {
var e Event
if err := DBQueryRow("SELECT id_event, txt, kind, time FROM events WHERE id_event=?", id).Scan(&e.Id, &e.Text, &e.Kind, &e.Time); err != nil {
return e, err
}
return e, nil
}
func NewEvent(txt string, kind string) (Event, error) {
if res, err := DBExec("INSERT INTO events (txt, kind, time) VALUES (?, ?, ?)", txt, kind, time.Now()); err != nil {
return Event{}, err
@ -62,3 +93,13 @@ func (e Event) Delete() (int64, error) {
return nb, err
}
}
func ClearEvents() (int64, error) {
if res, err := DBExec("DELETE FROM events"); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}

View file

@ -5,19 +5,21 @@ import (
"time"
)
var PartialValidation bool
type Exercice struct {
Id int64 `json:"id"`
Title string `json:"title"`
Statement string `json:"statement"`
Hint string `json:"hint"`
Depend *int64 `json:"depend"`
Gain int64 `json:"gain"`
VideoURI string `json:"videoURI"`
Id int64 `json:"id"`
Title string `json:"title"`
Statement string `json:"statement"`
Depend *int64 `json:"depend"`
Gain int64 `json:"gain"`
Coefficient float64 `json:"coefficient"`
VideoURI string `json:"videoURI"`
}
func GetExercice(id int64) (Exercice, error) {
var e Exercice
if err := DBQueryRow("SELECT id_exercice, title, statement, hint, depend, gain, video_uri FROM exercices WHERE id_exercice = ?", id).Scan(&e.Id, &e.Title, &e.Statement, &e.Hint, &e.Depend, &e.Gain, &e.VideoURI); err != nil {
if err := DBQueryRow("SELECT id_exercice, title, statement, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_exercice = ?", id).Scan(&e.Id, &e.Title, &e.Statement, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil {
return Exercice{}, err
}
@ -26,7 +28,7 @@ func GetExercice(id int64) (Exercice, error) {
func (t Theme) GetExercice(id int) (Exercice, error) {
var e Exercice
if err := DBQueryRow("SELECT id_exercice, title, statement, hint, depend, gain, video_uri FROM exercices WHERE id_theme = ? AND id_exercice = ?", t.Id, id).Scan(&e.Id, &e.Title, &e.Statement, &e.Hint, &e.Depend, &e.Gain, &e.VideoURI); err != nil {
if err := DBQueryRow("SELECT id_exercice, title, statement, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ? AND id_exercice = ?", t.Id, id).Scan(&e.Id, &e.Title, &e.Statement, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil {
return Exercice{}, err
}
@ -34,7 +36,7 @@ func (t Theme) GetExercice(id int) (Exercice, error) {
}
func GetExercices() ([]Exercice, error) {
if rows, err := DBQuery("SELECT id_exercice, title, statement, hint, depend, gain, video_uri FROM exercices"); err != nil {
if rows, err := DBQuery("SELECT id_exercice, title, statement, depend, gain, coefficient_cur, video_uri FROM exercices"); err != nil {
return nil, err
} else {
defer rows.Close()
@ -42,7 +44,7 @@ func GetExercices() ([]Exercice, error) {
var exos = make([]Exercice, 0)
for rows.Next() {
var e Exercice
if err := rows.Scan(&e.Id, &e.Title, &e.Statement, &e.Hint, &e.Depend, &e.Gain, &e.VideoURI); err != nil {
if err := rows.Scan(&e.Id, &e.Title, &e.Statement, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil {
return nil, err
}
exos = append(exos, e)
@ -56,7 +58,7 @@ func GetExercices() ([]Exercice, error) {
}
func (t Theme) GetExercices() ([]Exercice, error) {
if rows, err := DBQuery("SELECT id_exercice, title, statement, hint, depend, gain, video_uri FROM exercices WHERE id_theme = ?", t.Id); err != nil {
if rows, err := DBQuery("SELECT id_exercice, title, statement, depend, gain, coefficient_cur, video_uri FROM exercices WHERE id_theme = ?", t.Id); err != nil {
return nil, err
} else {
defer rows.Close()
@ -64,7 +66,7 @@ func (t Theme) GetExercices() ([]Exercice, error) {
var exos = make([]Exercice, 0)
for rows.Next() {
var e Exercice
if err := rows.Scan(&e.Id, &e.Title, &e.Statement, &e.Hint, &e.Depend, &e.Gain, &e.VideoURI); err != nil {
if err := rows.Scan(&e.Id, &e.Title, &e.Statement, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI); err != nil {
return nil, err
}
exos = append(exos, e)
@ -77,28 +79,28 @@ func (t Theme) GetExercices() ([]Exercice, error) {
}
}
func (t Theme) AddExercice(title string, statement string, hint string, depend *Exercice, gain int, videoURI string) (Exercice, error) {
func (t Theme) AddExercice(title string, statement string, depend *Exercice, gain int64, videoURI string) (Exercice, error) {
var dpd interface{}
if depend == nil {
dpd = nil
} else {
dpd = depend.Id
}
if res, err := DBExec("INSERT INTO exercices (id_theme, title, statement, hint, depend, gain, video_uri) VALUES (?, ?, ?, ?, ?, ?, ?)", t.Id, title, statement, hint, dpd, gain, videoURI); err != nil {
if res, err := DBExec("INSERT INTO exercices (id_theme, title, statement, depend, gain, video_uri) VALUES (?, ?, ?, ?, ?, ?)", t.Id, title, statement, dpd, gain, videoURI); err != nil {
return Exercice{}, err
} else if eid, err := res.LastInsertId(); err != nil {
return Exercice{}, err
} else {
if depend == nil {
return Exercice{eid, title, statement, hint, nil, int64(gain), videoURI}, nil
return Exercice{eid, title, statement, nil, gain, 1.0, videoURI}, nil
} else {
return Exercice{eid, title, statement, hint, &depend.Id, int64(gain), videoURI}, nil
return Exercice{eid, title, statement, &depend.Id, gain, 1.0, videoURI}, nil
}
}
}
func (e Exercice) Update() (int64, error) {
if res, err := DBExec("UPDATE exercices SET title = ?, statement = ?, hint = ?, depend = ?, gain = ?, video_uri = ? WHERE id_exercice = ?", e.Title, e.Statement, e.Hint, e.Depend, e.Gain, e.VideoURI, e.Id); err != nil {
if res, err := DBExec("UPDATE exercices SET title = ?, statement = ?, depend = ?, gain = ?, coefficient_cur = ?, video_uri = ? WHERE id_exercice = ?", e.Title, e.Statement, e.Depend, e.Gain, e.Coefficient, e.VideoURI, e.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
@ -156,7 +158,7 @@ func (e Exercice) NewTry(t Team) error {
}
func (e Exercice) Solved(t Team) error {
if _, err := DBExec("INSERT INTO exercice_solved (id_exercice, id_team, time) VALUES (?, ?, ?)", e.Id, t.Id, time.Now()); err != nil {
if _, err := DBExec("INSERT INTO exercice_solved (id_exercice, id_team, time, coefficient) VALUES (?, ?, ?, ?)", e.Id, t.Id, time.Now(), e.Coefficient); err != nil {
return err
} else {
return nil
@ -202,13 +204,14 @@ func (e Exercice) CheckResponse(resps map[string]string, t Team) (bool, error) {
valid := true
for _, key := range keys {
if _, ok := resps[key.Type]; !ok {
if res, ok := resps[key.Type]; !ok {
valid = false
break
}
if !key.Check(resps[key.Type]) {
valid = false
break
} else if !key.Check(res) {
if !PartialValidation || t.HasPartiallySolved(key) == nil {
valid = false
}
} else {
key.FoundBy(t)
}
}

View file

@ -3,6 +3,8 @@ package fic
import (
"bufio"
"crypto/sha1"
"encoding/hex"
"errors"
"io"
"os"
"path"
@ -10,6 +12,7 @@ import (
)
var FilesDir string
var OptionalDigest bool
type EFile struct {
Id int64 `json:"id"`
@ -66,8 +69,10 @@ func (e Exercice) GetFiles() ([]EFile, error) {
}
}
func (e Exercice) ImportFile(filePath string, origin string) (EFile, error) {
if fi, err := os.Stat(filePath); err != nil {
func (e Exercice) ImportFile(filePath string, origin string, digest []byte) (interface{}, error) {
if digest == nil && !OptionalDigest {
return EFile{}, errors.New("No digest given.")
} else if fi, err := os.Stat(filePath); err != nil {
return EFile{}, err
} else if fd, err := os.Open(filePath); err != nil {
return EFile{}, err
@ -79,9 +84,19 @@ func (e Exercice) ImportFile(filePath string, origin string) (EFile, error) {
if _, err := io.Copy(hash, reader); err != nil {
return EFile{}, err
}
result := hash.Sum(nil)
var result []byte
return e.AddFile(strings.TrimPrefix(filePath, FilesDir), origin, path.Base(filePath), hash.Sum(result), fi.Size())
if len(digest) != len(result) {
return EFile{}, errors.New("Digests doesn't match: calculated: " + hex.EncodeToString(result) + " vs. given: " + hex.EncodeToString(digest))
}
for k := range result {
if result[k] != digest[k] {
return EFile{}, errors.New("Digests doesn't match: calculated: " + hex.EncodeToString(result) + " vs. given: " + hex.EncodeToString(digest))
}
}
return e.AddFile(strings.TrimPrefix(filePath, FilesDir), origin, path.Base(filePath), result, fi.Size())
}
}

95
libfic/hint.go Normal file
View file

@ -0,0 +1,95 @@
package fic
import (
"path"
"strings"
)
type EHint struct {
Id int64 `json:"id"`
IdExercice int64 `json:"idExercice"`
Title string `json:"title"`
Content string `json:"content"`
File string `json:"file"`
Cost int64 `json:"cost"`
}
func treatHintContent(h *EHint) {
if strings.HasPrefix(h.Content, "$FILES") {
h.File = path.Join(FilesDir, strings.TrimPrefix(h.Content, "$FILES"))
h.Content = ""
}
}
func GetHint(id int64) (EHint, error) {
var h EHint
if err := DBQueryRow("SELECT id_hint, id_exercice, title, content, cost FROM exercice_hints WHERE id_hint = ?", id).Scan(&h.Id, &h.IdExercice, &h.Title, &h.Content, &h.Cost); err != nil {
return h, err
}
treatHintContent(&h)
return h, nil
}
func (e Exercice) GetHints() ([]EHint, error) {
if rows, err := DBQuery("SELECT id_hint, title, content, cost FROM exercice_hints WHERE id_exercice = ?", e.Id); err != nil {
return nil, err
} else {
defer rows.Close()
var hints = make([]EHint, 0)
for rows.Next() {
var h EHint
h.IdExercice = e.Id
if err := rows.Scan(&h.Id, &h.Title, &h.Content, &h.Cost); err != nil {
return nil, err
}
treatHintContent(&h)
hints = append(hints, h)
}
if err := rows.Err(); err != nil {
return nil, err
}
return hints, nil
}
}
func (e Exercice) AddHint(title string, content string, cost int64) (EHint, error) {
if res, err := DBExec("INSERT INTO exercice_hints (id_exercice, title, content, cost) VALUES (?, ?, ?, ?)", e.Id, title, content, cost); err != nil {
return EHint{}, err
} else if hid, err := res.LastInsertId(); err != nil {
return EHint{}, err
} else {
return EHint{hid, e.Id, title, content, "", cost}, nil
}
}
func (h EHint) Update() (int64, error) {
if res, err := DBExec("UPDATE exercice_hints SET id_exercice = ?, title = ?, content = ?, cost = ? WHERE id_hint = ?", h.IdExercice, h.Title, h.Content, h.Cost, h.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func (h EHint) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_hints WHERE id_hint = ?", h.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func (h EHint) GetExercice() (Exercice, error) {
var eid int64
if err := DBQueryRow("SELECT id_exercice FROM exercice_hints WHERE id_hint = ?", h.Id).Scan(&eid); err != nil {
return Exercice{}, err
}
return GetExercice(eid)
}

View file

@ -2,6 +2,7 @@ package fic
import (
"crypto/sha512"
"time"
)
type Key struct {
@ -88,3 +89,7 @@ func (k Key) Check(val string) bool {
return true
}
func (k Key) FoundBy(t Team) {
DBExec("INSERT INTO key_found (id_key, id_team, time) VALUES (?, ?, ?)", k.Id, t.Id, time.Now())
}

View file

@ -10,6 +10,15 @@ type Member struct {
Company string `json:"company"`
}
func GetMember(cnt int) (Team, error) {
var t Team
if err := DBQueryRow("SELECT T.id_team, T.initial_name, T.name, T.color FROM team_members M RIGHT OUTER JOIN teams T ON T.id_team = M.id_team LIMIT ?, 1", cnt - 1).Scan(&t.Id, &t.InitialName, &t.Name, &t.Color); err != nil {
return t, err
}
return t, nil
}
func (t Team) GetMembers() ([]Member, error) {
if rows, err := DBQuery("SELECT id_member, firstname, lastname, nickname, company FROM team_members WHERE id_team = ?", t.Id); err != nil {
return nil, err
@ -71,3 +80,13 @@ func (m Member) Delete() (int64, error) {
return nb, err
}
}
func (t Team) ClearMembers() (int64, error) {
if res, err := DBExec("DELETE FROM team_members WHERE id_team = ?", t.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}

30
libfic/reset.go Normal file
View file

@ -0,0 +1,30 @@
package fic
import ()
func truncateTable(tables ...string) (error) {
if _, err := DBExec("SET FOREIGN_KEY_CHECKS = 0;"); err != nil {
return err
}
for _, table := range tables {
if _, err := DBExec("TRUNCATE TABLE " + table + ";"); err != nil {
return err
}
}
if _, err := DBExec("SET FOREIGN_KEY_CHECKS = 1;"); err != nil {
return err
}
return nil
}
func ResetGame() (error) {
return truncateTable("team_hints", "key_found", "exercice_solved", "exercice_tries")
}
func ResetExercices() (error) {
return truncateTable("team_hints", "exercice_files", "key_found", "exercice_keys", "exercice_solved", "exercice_tries", "exercice_hints", "exercices", "themes")
}
func ResetTeams() (error) {
return truncateTable("team_hints", "key_found", "exercice_solved", "exercice_tries", "team_members", "teams")
}

137
libfic/stats.go Normal file
View file

@ -0,0 +1,137 @@
package fic
import (
"database/sql"
"fmt"
"time"
)
var FirstBlood = 0.12
var SubmissionCostBase = 0.5
func exoptsQuery(whereExo string) string {
return "SELECT S.id_team, S.time, E.gain AS points, coeff FROM (SELECT id_team, id_exercice, MIN(time) AS time, " + fmt.Sprintf("%f", FirstBlood) + " AS coeff FROM exercice_solved GROUP BY id_exercice UNION SELECT id_team, id_exercice, time, coefficient AS coeff FROM exercice_solved) S INNER JOIN exercices E ON S.id_exercice = E.id_exercice " + whereExo + " UNION ALL SELECT id_team, MAX(time) AS time, (FLOOR(COUNT(*)/10 - 1) * (FLOOR(COUNT(*)/10)))/0.2 + (FLOOR(COUNT(*)/10) * (COUNT(*)%10)) AS points, " + fmt.Sprintf("%f", SubmissionCostBase * -1) + " AS coeff FROM exercice_tries S " + whereExo + " GROUP BY id_exercice"
}
func rankQuery(whereTeam string) string {
return "SELECT A.id_team, SUM(A.points * A.coeff) AS score, MAX(A.time) AS time FROM (" + exoptsQuery("") + " UNION ALL SELECT D.id_team, D.time, H.cost AS points, -1.0 AS coeff FROM team_hints D INNER JOIN exercice_hints H ON H.id_hint = D.id_hint HAVING points != 0) A " + whereTeam + " GROUP BY A.id_team ORDER BY score DESC, time ASC"
}
// Points
func (e Exercice) EstimateGain(t Team, solved bool) (float64, error) {
var pts float64
err := DBQueryRow("SELECT SUM(A.points * A.coeff) AS score FROM (" + exoptsQuery("WHERE S.id_team = ? AND S.id_exercice = ?") + ") A GROUP BY id_team", t.Id, e.Id, t.Id, e.Id).Scan(&pts)
if solved {
return pts, err
} else {
pts += float64(e.Gain) * e.Coefficient
if e.SolvedCount() <= 0 {
pts += float64(e.Gain) * FirstBlood
}
return pts, nil
}
}
func (t Team) GetPoints() (float64, error) {
var tid *int64
var nb *float64
var tzzz *time.Time
err := DBQueryRow(rankQuery("WHERE A.id_team = ?"), t.Id).Scan(&tid, &nb, &tzzz)
if nb != nil {
return *nb, err
} else {
return 0, err
}
}
func GetRank() (map[int64]int, error) {
if rows, err := DBQuery(rankQuery("")); err != nil {
return nil, err
} else {
defer rows.Close()
rank := map[int64]int{}
nteam := 0
for rows.Next() {
nteam += 1
var tid int64
var score float64
var tzzz time.Time
if err := rows.Scan(&tid, &score, &tzzz); err != nil {
return nil, err
}
rank[tid] = nteam
}
if err := rows.Err(); err != nil {
return nil, err
}
return rank, nil
}
}
// Tries
func GetTries(t *Team, e *Exercice) ([]time.Time, error) {
var rows *sql.Rows
var err error
if t == nil {
if e == nil {
rows, err = DBQuery("SELECT time FROM exercice_tries ORDER BY time ASC")
} else {
rows, err = DBQuery("SELECT time FROM exercice_tries WHERE id_exercice = ? ORDER BY time ASC", e.Id)
}
} else {
if e == nil {
rows, err = DBQuery("SELECT time FROM exercice_tries WHERE id_team = ? ORDER BY time ASC", t.Id)
} else {
rows, err = DBQuery("SELECT time FROM exercice_tries WHERE id_team = ? AND id_exercice = ? ORDER BY time ASC", t.Id, e.Id)
}
}
if err != nil {
return nil, err
} else {
defer rows.Close()
times := make([]time.Time, 0)
for rows.Next() {
var tm time.Time
if err := rows.Scan(&tm); err != nil {
return nil, err
}
times = append(times, tm)
}
if err := rows.Err(); err != nil {
return nil, err
}
return times, nil
}
}
func GetTryRank() ([]int64, error) {
if rows, err := DBQuery("SELECT id_team, COUNT(*) AS score FROM exercice_tries GROUP BY id_team HAVING score > 0 ORDER BY score DESC"); err != nil {
return nil, err
} else {
defer rows.Close()
rank := make([]int64, 0)
for rows.Next() {
var tid int64
var score int64
if err := rows.Scan(&tid, &score); err != nil {
return nil, err
}
rank = append(rank, tid)
}
if err := rows.Err(); err != nil {
return nil, err
}
return rank, nil
}
}

View file

@ -1,11 +1,12 @@
package fic
import (
"database/sql"
"fmt"
"regexp"
"time"
)
var UnlockedChallenges bool
type Team struct {
Id int64 `json:"id"`
InitialName string `json:"initialName"`
@ -13,6 +14,8 @@ type Team struct {
Color uint32 `json:"color"`
}
// Access functions
func GetTeams() ([]Team, error) {
if rows, err := DBQuery("SELECT id_team, initial_name, name, color FROM teams"); err != nil {
return nil, err
@ -53,8 +56,13 @@ func GetTeamByInitialName(initialName string) (Team, error) {
return t, nil
}
// CRUD method
func CreateTeam(name string, color uint32) (Team, error) {
if res, err := DBExec("INSERT INTO teams (initial_name, name, color) VALUES (?, ?, ?)", name, name, color); err != nil {
re := regexp.MustCompile("[^a-zA-Z0-9]+")
initialName := re.ReplaceAllLiteralString(name, "_")
if res, err := DBExec("INSERT INTO teams (initial_name, name, color) VALUES (?, ?, ?)", initialName, name, color); err != nil {
return Team{}, err
} else if tid, err := res.LastInsertId(); err != nil {
return Team{}, err
@ -83,67 +91,11 @@ func (t Team) Delete() (int64, error) {
}
}
func (t Team) GetPoints() (int64, error) {
var nb *int64
err := DBQueryRow("SELECT SUM(E.gain) FROM exercice_solved S INNER JOIN exercices E ON E.id_exercice = S.id_exercice WHERE id_team = ?", t.Id).Scan(&nb)
if nb != nil {
return *nb, err
} else {
return 0, err
}
}
func GetRank() (map[int64]int, error) {
if rows, err := DBQuery("SELECT id_team, SUM(E.gain) AS score, MAX(S.time) FROM exercice_solved S INNER JOIN exercices E ON E.id_exercice = S.id_exercice GROUP BY id_team HAVING score > 0 ORDER BY score DESC, time ASC"); err != nil {
return nil, err
} else {
defer rows.Close()
rank := map[int64]int{}
nteam := 0
for rows.Next() {
nteam += 1
var tid int64
var score int64
var tzzz time.Time
if err := rows.Scan(&tid, &score, &tzzz); err != nil {
return nil, err
}
rank[tid] = nteam
}
if err := rows.Err(); err != nil {
return nil, err
}
return rank, nil
}
}
func GetTryRank() ([]int64, error) {
if rows, err := DBQuery("SELECT id_team, COUNT(*) AS score FROM exercice_tries GROUP BY id_team HAVING score > 0 ORDER BY score DESC"); err != nil {
return nil, err
} else {
defer rows.Close()
rank := make([]int64, 0)
for rows.Next() {
var tid int64
var score int64
if err := rows.Scan(&tid, &score); err != nil {
return nil, err
}
rank = append(rank, tid)
}
if err := rows.Err(); err != nil {
return nil, err
}
return rank, nil
}
}
// Exercice related functions
func (t Team) HasAccess(e Exercice) bool {
if e.Depend == nil {
if e.Depend == nil || UnlockedChallenges {
return true
} else {
ed := Exercice{}
@ -169,42 +121,26 @@ func NbTry(t *Team, e Exercice) int {
}
}
func GetTries(t *Team, e *Exercice) ([]time.Time, error) {
var rows *sql.Rows
var err error
func (t Team) HasHint(h EHint) (bool) {
var tm *time.Time
DBQueryRow("SELECT MIN(time) FROM team_hints WHERE id_team = ? AND id_hint = ?", t.Id, h.Id).Scan(&tm)
return tm != nil
}
if t == nil {
if e == nil {
rows, err = DBQuery("SELECT time FROM exercice_tries ORDER BY time ASC")
} else {
rows, err = DBQuery("SELECT time FROM exercice_tries WHERE id_exercice = ? ORDER BY time ASC", e.Id)
}
func (t Team) OpenHint(h EHint) (error) {
_, err := DBExec("INSERT INTO team_hints (id_team, id_hint, time) VALUES (?, ?, ?)", t.Id, h.Id, time.Now())
return err
}
func (t Team) CountTries(e Exercice) (int64, time.Time) {
var nb *int64
var tm *time.Time
if DBQueryRow("SELECT COUNT(id_exercice), MAX(time) FROM exercice_tries WHERE id_team = ? AND id_exercice = ?", t.Id, e.Id).Scan(&nb, &tm); tm == nil {
return 0, time.Unix(0, 0)
} else if nb == nil {
return 0, *tm
} else {
if e == nil {
rows, err = DBQuery("SELECT time FROM exercice_tries WHERE id_team = ? ORDER BY time ASC", t.Id)
} else {
rows, err = DBQuery("SELECT time FROM exercice_tries WHERE id_team = ? AND id_exercice = ? ORDER BY time ASC", t.Id, e.Id)
}
}
if err != nil {
return nil, err
} else {
defer rows.Close()
times := make([]time.Time, 0)
for rows.Next() {
var tm time.Time
if err := rows.Scan(&tm); err != nil {
return nil, err
}
times = append(times, tm)
}
if err := rows.Err(); err != nil {
return nil, err
}
return times, nil
return *nb, *tm
}
}
@ -212,13 +148,7 @@ func (t Team) HasSolved(e Exercice) (bool, time.Time, int64) {
var nb *int64
var tm *time.Time
if DBQueryRow("SELECT MIN(time) FROM exercice_solved WHERE id_team = ? AND id_exercice = ?", t.Id, e.Id).Scan(&tm); tm == nil {
if DBQueryRow("SELECT COUNT(id_exercice), MAX(time) FROM exercice_tries WHERE id_team = ? AND id_exercice = ?", t.Id, e.Id).Scan(&nb, &tm); tm == nil {
return false, time.Unix(0, 0), 0
} else if nb == nil {
return false, *tm, 0
} else {
return false, *tm, *nb
}
return false, time.Unix(0, 0), 0
} else if DBQueryRow("SELECT COUNT(id_exercice) FROM exercice_solved WHERE id_exercice = ? AND time < ?", e.Id, tm).Scan(&nb); nb == nil {
return true, *tm, 0
} else {
@ -236,126 +166,8 @@ func IsSolved(e Exercice) (int, time.Time) {
}
}
type statLine struct {
Tip string `json:"tip"`
Total int `json:"total"`
Solved int `json:"solved"`
Tried int `json:"tried"`
Tries int `json:"tries"`
}
type teamStats struct {
Levels []statLine `json:"levels"`
Themes []statLine `json:"themes"`
}
func (s *teamStats) GetLevel(level int) *statLine {
level -= 1
for len(s.Levels) <= level {
s.Levels = append(s.Levels, statLine{
fmt.Sprintf("Level %d", (len(s.Levels) + 1)),
0,
0,
0,
0,
})
}
return &s.Levels[level]
}
func (t Team) GetStats() (interface{}, error) {
return GetTeamsStats(&t)
}
func GetTeamsStats(t *Team) (interface{}, error) {
stat := teamStats{}
if themes, err := GetThemes(); err != nil {
return nil, err
} else {
for _, theme := range themes {
total := 0
solved := 0
tried := 0
tries := 0
if exercices, err := theme.GetExercices(); err != nil {
return nil, err
} else {
for _, exercice := range exercices {
var lvl int
if lvl, err = exercice.GetLevel(); err != nil {
return nil, err
}
sLvl := stat.GetLevel(lvl)
total += 1
sLvl.Total += 1
if t != nil {
if b, _, _ := t.HasSolved(exercice); b {
solved += 1
sLvl.Solved += 1
}
} else {
if n, _ := IsSolved(exercice); n > 0 {
solved += 1
sLvl.Solved += 1
}
}
try := NbTry(t, exercice)
if try > 0 {
tried += 1
tries += try
sLvl.Tried += 1
sLvl.Tries += try
}
}
}
stat.Themes = append(stat.Themes, statLine{
theme.Name,
total,
solved,
tried,
tries,
})
}
return stat, nil
}
}
type exportedTeam struct {
Name string `json:"name"`
Color string `json:"color"`
Rank int `json:"rank"`
Points int64 `json:"score"`
}
func ExportTeams() (interface{}, error) {
if teams, err := GetTeams(); err != nil {
return nil, err
} else if rank, err := GetRank(); err != nil {
return nil, err
} else {
ret := map[string]exportedTeam{}
for _, team := range teams {
if points, err := team.GetPoints(); err != nil {
return nil, err
} else {
ret[fmt.Sprintf("%d", team.Id)] = exportedTeam{
team.Name,
fmt.Sprintf("#%x", team.Color),
rank[team.Id],
points,
}
}
}
return ret, nil
}
func (t Team) HasPartiallySolved(k Key) (*time.Time) {
var tm *time.Time
DBQueryRow("SELECT MIN(time) FROM key_found WHERE id_team = ? AND id_key = ?", t.Id, k.Id).Scan(&tm)
return tm
}

33
libfic/team_export.go Normal file
View file

@ -0,0 +1,33 @@
package fic
import (
"fmt"
)
type exportedTeam struct {
Name string `json:"name"`
Color string `json:"color"`
Rank int `json:"rank"`
Points float64 `json:"score"`
}
func ExportTeams() (interface{}, error) {
if teams, err := GetTeams(); err != nil {
return nil, err
} else if rank, err := GetRank(); err != nil {
return nil, err
} else {
ret := map[string]exportedTeam{}
for _, team := range teams {
points, _ := team.GetPoints()
ret[fmt.Sprintf("%d", team.Id)] = exportedTeam{
team.Name,
fmt.Sprintf("#%x", team.Color),
rank[team.Id],
points,
}
}
return ret, nil
}
}

View file

@ -3,7 +3,9 @@ package fic
import (
"encoding/hex"
"fmt"
"log"
"time"
"path"
)
type myTeamFile struct {
@ -12,16 +14,24 @@ type myTeamFile struct {
Checksum string `json:"checksum"`
Size int64 `json:"size"`
}
type myTeamHint struct {
HintId int64 `json:"id"`
Title string `json:"title"`
Content *string `json:"content"`
File *string `json:"file"`
Cost int64 `json:"cost"`
}
type myTeamExercice struct {
ThemeId int `json:"theme_id"`
Statement string `json:"statement"`
Hint string `json:"hint"`
Gain int64 `json:"gain"`
Hints []myTeamHint `json:"hints"`
Gain float64 `json:"gain"`
Files []myTeamFile `json:"files"`
Keys []string `json:"keys"`
Solved bool `json:"solved"`
SolvedMat []bool `json:"solved_matrix"`
SolvedTime time.Time `json:"solved_time"`
SolvedNumber int64 `json:"solved_number"`
SolvedRank int64 `json:"solved_rank"`
Tries int64 `json:"tries"`
VideoURI string `json:"video_uri"`
}
type myTeam struct {
@ -34,19 +44,23 @@ type myTeam struct {
func MyJSONTeam(t *Team, started bool) (interface{}, error) {
ret := myTeam{}
// Fill information about the team
if t == nil {
ret.Id = 0
} else {
ret.Name = t.Name
ret.Id = t.Id
ret.Points, _ = t.GetPoints()
points, _ := t.GetPoints()
ret.Points = int64(points)
if members, err := t.GetMembers(); err == nil {
ret.Members = members
}
}
ret.Exercices = map[string]myTeamExercice{}
// Fill exercices, only if the challenge is started
ret.Exercices = map[string]myTeamExercice{}
if exos, err := GetExercices(); err != nil {
return ret, err
} else if started {
@ -57,15 +71,58 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) {
exercice.ThemeId = tid
}
exercice.Statement = e.Statement
exercice.Hint = e.Hint
if t == nil {
exercice.VideoURI = e.VideoURI
exercice.Solved = true
exercice.SolvedNumber = e.TriedCount()
exercice.SolvedRank = 1
exercice.Tries = e.TriedCount()
exercice.Gain = float64(e.Gain) * e.Coefficient
} else {
exercice.Solved, exercice.SolvedTime, exercice.SolvedNumber = t.HasSolved(e)
var solved bool
solved, exercice.SolvedTime, exercice.SolvedRank = t.HasSolved(e)
if solved {
exercice.Tries, _ = t.CountTries(e)
} else {
exercice.Tries, exercice.SolvedTime = t.CountTries(e)
}
if gain, err := e.EstimateGain(*t, solved); err == nil {
exercice.Gain = gain
} else {
log.Println("ERROR during gain estimation:", err)
}
}
// Expose exercice files
exercice.Files = []myTeamFile{}
if files, err := e.GetFiles(); err != nil {
return nil, err
} else {
for _, f := range files {
exercice.Files = append(exercice.Files, myTeamFile{path.Join(FilesDir, f.Path), f.Name, hex.EncodeToString(f.Checksum), f.Size})
}
}
// Expose exercice hints
exercice.Hints = []myTeamHint{}
if hints, err := e.GetHints(); err != nil {
return nil, err
} else {
for _, h := range hints {
if t == nil || t.HasHint(h) {
exercice.Hints = append(exercice.Hints, myTeamHint{h.Id, h.Title, &h.Content, &h.File, h.Cost})
} else {
exercice.Hints = append(exercice.Hints, myTeamHint{h.Id, h.Title, nil, nil, h.Cost})
}
}
}
// Expose exercice keys
exercice.Keys = []string{}
if keys, err := e.GetKeys(); err != nil {
@ -76,20 +133,14 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) {
exercice.Keys = append(exercice.Keys, fmt.Sprintf("%x", k.Value)+k.Type)
} else {
exercice.Keys = append(exercice.Keys, k.Type)
if PartialValidation {
exercice.SolvedMat = append(exercice.SolvedMat, t.HasPartiallySolved(k) != nil)
}
}
}
}
exercice.Files = []myTeamFile{}
if files, err := e.GetFiles(); err != nil {
return nil, err
} else {
for _, f := range files {
exercice.Files = append(exercice.Files, myTeamFile{f.Path, f.Name, hex.EncodeToString(f.Checksum), f.Size})
}
}
// Hash table ordered by exercice Id
ret.Exercices[fmt.Sprintf("%d", e.Id)] = exercice
}
}

101
libfic/team_stats.go Normal file
View file

@ -0,0 +1,101 @@
package fic
import (
"fmt"
)
type statLine struct {
Tip string `json:"tip"`
Total int `json:"total"`
Solved int `json:"solved"`
Tried int `json:"tried"`
Tries int `json:"tries"`
}
type teamStats struct {
Levels []statLine `json:"levels"`
Themes map[int64]statLine `json:"themes"`
}
func (s *teamStats) GetLevel(level int) *statLine {
level -= 1
for len(s.Levels) <= level {
s.Levels = append(s.Levels, statLine{
fmt.Sprintf("Level %d", (len(s.Levels) + 1)),
0,
0,
0,
0,
})
}
return &s.Levels[level]
}
func (t Team) GetStats() (interface{}, error) {
return GetTeamsStats(&t)
}
func GetTeamsStats(t *Team) (interface{}, error) {
stat := teamStats{
[]statLine{},
map[int64]statLine{},
}
if themes, err := GetThemes(); err != nil {
return nil, err
} else {
for _, theme := range themes {
total := 0
solved := 0
tried := 0
tries := 0
if exercices, err := theme.GetExercices(); err != nil {
return nil, err
} else {
for _, exercice := range exercices {
var lvl int
if lvl, err = exercice.GetLevel(); err != nil {
return nil, err
}
sLvl := stat.GetLevel(lvl)
total += 1
sLvl.Total += 1
if t != nil {
if b, _, _ := t.HasSolved(exercice); b {
solved += 1
sLvl.Solved += 1
}
} else {
if n, _ := IsSolved(exercice); n > 0 {
solved += 1
sLvl.Solved += 1
}
}
try := NbTry(t, exercice)
if try > 0 {
tried += 1
tries += try
sLvl.Tried += 1
sLvl.Tries += try
}
}
}
stat.Themes[theme.Id] = statLine{
theme.Name,
total,
solved,
tried,
tries,
}
}
return stat, nil
}
}

View file

@ -72,10 +72,11 @@ func (t Theme) Delete() (int64, error) {
}
type ExportedExercice struct {
Title string `json:"title"`
Gain int64 `json:"gain"`
Solved int64 `json:"solved"`
Tried int64 `json:"tried"`
Title string `json:"title"`
Gain int64 `json:"gain"`
Coeff float64 `json:"curcoeff"`
Solved int64 `json:"solved"`
Tried int64 `json:"tried"`
}
type exportedTheme struct {
@ -98,13 +99,14 @@ func ExportThemes() (interface{}, error) {
exos[fmt.Sprintf("%d", exercice.Id)] = ExportedExercice{
exercice.Title,
exercice.Gain,
exercice.Coefficient,
exercice.SolvedCount(),
exercice.TriedTeamCount(),
}
}
ret[fmt.Sprintf("%d", theme.Id)] = exportedTheme{
theme.Name,
theme.Authors[:len(theme.Authors)-1],
theme.Authors,
exos,
}
}

7
password_paper/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
missfont.log
pass.aux
pass.fdb_latexmk
pass.fls
pass.log
pass.out
teams.pass

Some files were not shown because too many files have changed in this diff Show more