From fae660b0e43d4fbc2074a01c73ad76cc956de993 Mon Sep 17 00:00:00 2001 From: Jay McCarthy Date: Wed, 7 Nov 2012 21:29:58 -0700 Subject: [PATCH] Release Planet 2 (beta) This was developed in a different repository, so the history will be archived there: https://github.com/jeapostrophe/galaxy --- collects/meta/planet2-index/basic/main.rkt | 44 + .../meta/planet2-index/official/.gitignore | 1 + .../meta/planet2-index/official/gravatar.rkt | 52 ++ collects/meta/planet2-index/official/main.rkt | 652 ++++++++++++++ .../official/static/sorttable.js | 515 +++++++++++ .../planet2-index/official/static/style.css | 148 ++++ .../planet2-index/planet-compat/.gitignore | 1 + .../meta/planet2-index/planet-compat/info.rkt | 3 + .../meta/planet2-index/planet-compat/main.rkt | 395 +++++++++ collects/meta/planet2-index/sync.sh | 12 + collects/meta/props | 6 + collects/planet2/.gitignore | 1 + collects/planet2/commands.rkt | 101 +++ collects/planet2/info.rkt | 7 + collects/planet2/lib.rkt | 809 ++++++++++++++++++ collects/planet2/main.rkt | 91 ++ collects/planet2/raco.rkt | 2 + collects/planet2/scribblings/planet2.scrbl | 667 +++++++++++++++ collects/planet2/util-plt.rkt | 46 + collects/planet2/util.rkt | 68 ++ collects/tests/planet2/info.rkt | 3 + collects/tests/planet2/shelly.rkt | 134 +++ collects/tests/planet2/test-pkgs/.gitignore | 5 + .../test-pkgs/pkg-a-first/pkg-a/main.rkt | 3 + .../test-pkgs/pkg-a-second/pkg-a/main.rkt | 4 + .../test-pkgs/pkg-a-third/pkg-a/main.rkt | 3 + .../test-pkgs/pkg-a-third/pkg-b/main.rkt | 2 + .../test-pkgs/pkg-b-first/pkg-b/main.rkt | 3 + .../test-pkgs/pkg-b-second/METADATA.rktd | 1 + .../pkg-b-second/pkg-b/contains-dep.rkt | 9 + .../test-pkgs/pkg-b-second/pkg-b/main.rkt | 3 + .../planet2-test1/conflict.rkt | 2 + .../planet2-test1/main.rkt | 4 + .../planet2-test1/conflict.rkt | 2 + .../planet2-test1/main.rkt | 4 + .../planet2-test1/update.rkt | 2 + .../planet2-test1-not-conflict/README | 1 + .../test-pkgs/planet2-test1-staging/a.rkt | 4 + .../planet2-test1/conflict.rkt | 2 + .../planet2-test1-v2/planet2-test1/main.rkt | 4 + .../planet2-test1-v2/planet2-test1/update.rkt | 2 + .../planet2/test-pkgs/planet2-test1/README | 1 + .../planet2-test1/planet2-test1/conflict.rkt | 2 + .../planet2-test1/planet2-test1/main.rkt | 4 + .../planet2-test1/planet2-test1/update.rkt | 2 + .../test-pkgs/planet2-test2/METADATA.rktd | 1 + .../planet2-test2/contains-dep.rkt | 3 + .../planet2-test2/planet2-test2/main.rkt | 4 + .../test-pkgs/racket-conflict/racket/list.rkt | 2 + .../test-pkgs/raco-pkg/raco-pkg/info.rkt | 5 + .../test-pkgs/raco-pkg/raco-pkg/main.rkt | 3 + collects/tests/planet2/test.rkt | 42 + collects/tests/planet2/tests-basic.rkt | 25 + collects/tests/planet2/tests-checksums.rkt | 72 ++ collects/tests/planet2/tests-config.rkt | 11 + collects/tests/planet2/tests-conflicts.rkt | 61 ++ collects/tests/planet2/tests-create.rkt | 59 ++ collects/tests/planet2/tests-deps.rkt | 119 +++ collects/tests/planet2/tests-install.rkt | 98 +++ collects/tests/planet2/tests-locking.rkt | 42 + collects/tests/planet2/tests-main-server.rkt | 10 + collects/tests/planet2/tests-network.rkt | 21 + collects/tests/planet2/tests-overwrite.rkt | 16 + collects/tests/planet2/tests-planet.rkt | 22 + collects/tests/planet2/tests-raco.rkt | 30 + collects/tests/planet2/tests-remove.rkt | 72 ++ collects/tests/planet2/tests-update-auto.rkt | 71 ++ collects/tests/planet2/tests-update-deps.rkt | 110 +++ collects/tests/planet2/tests-update.rkt | 100 +++ collects/tests/planet2/util.rkt | 150 ++++ 70 files changed, 4976 insertions(+) create mode 100644 collects/meta/planet2-index/basic/main.rkt create mode 100644 collects/meta/planet2-index/official/.gitignore create mode 100644 collects/meta/planet2-index/official/gravatar.rkt create mode 100644 collects/meta/planet2-index/official/main.rkt create mode 100644 collects/meta/planet2-index/official/static/sorttable.js create mode 100644 collects/meta/planet2-index/official/static/style.css create mode 100644 collects/meta/planet2-index/planet-compat/.gitignore create mode 100644 collects/meta/planet2-index/planet-compat/info.rkt create mode 100644 collects/meta/planet2-index/planet-compat/main.rkt create mode 100755 collects/meta/planet2-index/sync.sh create mode 100644 collects/planet2/.gitignore create mode 100644 collects/planet2/commands.rkt create mode 100644 collects/planet2/info.rkt create mode 100644 collects/planet2/lib.rkt create mode 100644 collects/planet2/main.rkt create mode 100644 collects/planet2/raco.rkt create mode 100644 collects/planet2/scribblings/planet2.scrbl create mode 100644 collects/planet2/util-plt.rkt create mode 100644 collects/planet2/util.rkt create mode 100644 collects/tests/planet2/info.rkt create mode 100644 collects/tests/planet2/shelly.rkt create mode 100644 collects/tests/planet2/test-pkgs/.gitignore create mode 100644 collects/tests/planet2/test-pkgs/pkg-a-first/pkg-a/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/pkg-a-second/pkg-a/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/pkg-a-third/pkg-a/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/pkg-a-third/pkg-b/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/pkg-b-first/pkg-b/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/pkg-b-second/METADATA.rktd create mode 100644 collects/tests/planet2/test-pkgs/pkg-b-second/pkg-b/contains-dep.rkt create mode 100644 collects/tests/planet2/test-pkgs/pkg-b-second/pkg-b/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-conflict/planet2-test1/conflict.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-conflict/planet2-test1/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-manifest-error/planet2-test1/conflict.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-manifest-error/planet2-test1/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-manifest-error/planet2-test1/update.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-not-conflict/README create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-staging/a.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-v2/planet2-test1/conflict.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-v2/planet2-test1/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1-v2/planet2-test1/update.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1/README create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1/planet2-test1/conflict.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1/planet2-test1/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test1/planet2-test1/update.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test2/METADATA.rktd create mode 100644 collects/tests/planet2/test-pkgs/planet2-test2/planet2-test2/contains-dep.rkt create mode 100644 collects/tests/planet2/test-pkgs/planet2-test2/planet2-test2/main.rkt create mode 100644 collects/tests/planet2/test-pkgs/racket-conflict/racket/list.rkt create mode 100644 collects/tests/planet2/test-pkgs/raco-pkg/raco-pkg/info.rkt create mode 100644 collects/tests/planet2/test-pkgs/raco-pkg/raco-pkg/main.rkt create mode 100644 collects/tests/planet2/test.rkt create mode 100644 collects/tests/planet2/tests-basic.rkt create mode 100644 collects/tests/planet2/tests-checksums.rkt create mode 100644 collects/tests/planet2/tests-config.rkt create mode 100644 collects/tests/planet2/tests-conflicts.rkt create mode 100644 collects/tests/planet2/tests-create.rkt create mode 100644 collects/tests/planet2/tests-deps.rkt create mode 100644 collects/tests/planet2/tests-install.rkt create mode 100644 collects/tests/planet2/tests-locking.rkt create mode 100644 collects/tests/planet2/tests-main-server.rkt create mode 100644 collects/tests/planet2/tests-network.rkt create mode 100644 collects/tests/planet2/tests-overwrite.rkt create mode 100644 collects/tests/planet2/tests-planet.rkt create mode 100644 collects/tests/planet2/tests-raco.rkt create mode 100644 collects/tests/planet2/tests-remove.rkt create mode 100644 collects/tests/planet2/tests-update-auto.rkt create mode 100644 collects/tests/planet2/tests-update-deps.rkt create mode 100644 collects/tests/planet2/tests-update.rkt create mode 100644 collects/tests/planet2/util.rkt diff --git a/collects/meta/planet2-index/basic/main.rkt b/collects/meta/planet2-index/basic/main.rkt new file mode 100644 index 0000000000..f864ae269d --- /dev/null +++ b/collects/meta/planet2-index/basic/main.rkt @@ -0,0 +1,44 @@ +#lang racket/base +(require racket/list + racket/contract + web-server/http + web-server/dispatch) + +(define (response/sexpr v) + (response 200 #"Okay" (current-seconds) + #"text/s-expr" empty + (λ (op) (write v op)))) + +(define (planet2-index/basic get-pkgs pkg-name->info) + (define (write-info req pkg-name) + (response/sexpr (pkg-name->info pkg-name))) + (define (display-info req pkg-name) + (define info (pkg-name->info pkg-name)) + (response/xexpr + `(html + (body + (h1 ,pkg-name) + (p (a ([href ,(hash-ref info 'source)]) ,(hash-ref info 'source))) + (p ,(hash-ref info 'checksum)))))) + (define (list-pkgs req) + (response/xexpr + `(html + (body + (table + (tr (th "Package")) + ,@(for/list ([n (in-list (sort (get-pkgs) string<=?))]) + `(tr + (td (a ([href ,(get-url display-info n)]) ,n))))))))) + (define-values (dispatch get-url) + (dispatch-rules + [() list-pkgs] + [("") list-pkgs] + [("pkg" (string-arg) "display") display-info] + [("pkg" (string-arg)) write-info])) + dispatch) + +(provide/contract + [planet2-index/basic + (-> (-> (listof string?)) + (-> string? (hash/c symbol? any/c)) + (-> request? response?))]) diff --git a/collects/meta/planet2-index/official/.gitignore b/collects/meta/planet2-index/official/.gitignore new file mode 100644 index 0000000000..cd3d01855c --- /dev/null +++ b/collects/meta/planet2-index/official/.gitignore @@ -0,0 +1 @@ +/root diff --git a/collects/meta/planet2-index/official/gravatar.rkt b/collects/meta/planet2-index/official/gravatar.rkt new file mode 100644 index 0000000000..067243cfb7 --- /dev/null +++ b/collects/meta/planet2-index/official/gravatar.rkt @@ -0,0 +1,52 @@ +#lang racket/base +(require racket/string + racket/contract + xml + xml/path + racket/port + net/url + file/md5 + planet2/util) + +(define (gravatar-hash email) + (bytes->string/utf-8 + (md5 + (string-downcase + (string-trim email))))) + +(module+ test + (require rackunit) + (check-equal? (gravatar-hash "MyEmailAddress@example.com ") + "0bc83cb571cd1c50ba6f3e8a78ef1346") + (check-equal? (gravatar-hash "MyEmailAddress@example.com ") + (gravatar-hash " MyEmailAddress@example.com "))) + +(define (gravatar-image-url email) + (format "https://secure.gravatar.com/avatar/~a.jpg?d=retro" + (gravatar-hash email))) + +(define (gravatar-profile email) + (parameterize ([collapse-whitespace #t] + [xexpr-drop-empty-attributes #t]) + (call/input-url+200 + (string->url + (format "http://www.gravatar.com/~a.xml" + (gravatar-hash email))) + (compose string->xexpr port->string)))) + +(define (gravatar-display-name email) + (define profile (gravatar-profile email)) + (and profile + (se-path* '(response entry displayName) + profile))) + +(module+ test + (check-equal? (gravatar-display-name "jay.mccarthy@gmail.com") + "Jay McCarthy") + (check-equal? (gravatar-display-name "jay@racket-lang.org") + #f)) + +(provide/contract + [gravatar-display-name (-> string? (or/c string? false/c))] + [gravatar-profile (-> string? xexpr?)] + [gravatar-image-url (-> string? string?)]) diff --git a/collects/meta/planet2-index/official/main.rkt b/collects/meta/planet2-index/official/main.rkt new file mode 100644 index 0000000000..daea39eff8 --- /dev/null +++ b/collects/meta/planet2-index/official/main.rkt @@ -0,0 +1,652 @@ +#lang racket/base +(require web-server/http + web-server/servlet-env + racket/file + racket/function + racket/runtime-path + web-server/dispatch + planet2/util + racket/match + racket/package + racket/system + racket/date + racket/string + web-server/servlet + web-server/formlets + racket/bool + racket/list + net/sendmail + meta/planet2-index/basic/main + web-server/http/id-cookie + file/sha1) + +(define-syntax-rule (while cond e ...) + (let loop () + (when cond + e ... + (loop)))) + +(define (snoc l x) + (append l (list x))) + +(define (salty str) + (sha1 (open-input-string str))) + +(define-runtime-path src ".") + +(define-runtime-path root "root") +(make-directory* root) +(define secret-key + (make-secret-salt/file + (build-path root "secret.key"))) +(define users-path (build-path root "users")) +(make-directory* users-path) + +(module+ main + (define users-old-path (build-path root "users.old")) + (when (directory-exists? users-old-path) + (for ([u (in-list (directory-list users-old-path))]) + (define uop (build-path users-old-path u)) + (display-to-file (salty (file->string uop)) + (build-path users-path u)) + (delete-file uop)) + (delete-directory users-old-path))) + +(define pkgs-path (build-path root "pkgs")) +(make-directory* pkgs-path) + +(define id-cookie-name "id") + +;; XXX Add a caching system +(define (package-list) + (sort (map path->string (directory-list pkgs-path)) + string-ci<=?)) +(define (package-exists? pkg-name) + (file-exists? (build-path pkgs-path pkg-name))) +(define (package-remove! pkg-name) + (delete-file (build-path pkgs-path pkg-name))) +(define (package-info pkg-name) + (file->value (build-path pkgs-path pkg-name))) +(define (package-info-set! pkg-name i) + (write-to-file i (build-path pkgs-path pkg-name) + #:exists 'replace)) + +(define (package-ref pkg-info key) + (hash-ref pkg-info key + (λ () + (match key + [(or 'author 'checksum 'source) + (error 'planet2 "Package ~e is missing a required field: ~e" + (hash-ref pkg-info 'name) key)] + ['tags + empty] + [(or 'last-checked 'last-edit 'last-updated) + -inf.0])))) + +(define-values (main-dispatch main-url) + (dispatch-rules + [() page/main] + [("") page/main] + [("info" (string-arg)) page/info] + [("search" (string-arg) ...) page/search] + [("query" "search" (string-arg) ...) page/search/query] + [("account" "login") page/login] + [("account" "logout") page/logout] + [("manage") page/manage] + [("manage" "update") page/manage/update] + [("manage" "edit" (string-arg)) page/manage/edit] + [("manage" "upload") page/manage/upload] + [else basic-start])) + +(define (page/main req) + (redirect-to (main-url page/search empty))) + +(define (format-time s) + (if s + (parameterize ([date-display-format 'rfc2822]) + (date->string (seconds->date s #f) #t)) + "")) + +(define (package-url->useful-url pkg-url-str) + (define pkg-url + (string->url pkg-url-str)) + (match (url-scheme pkg-url) + ["github" + (match-define (list* user repo branch path) + (url-path pkg-url)) + (url->string + (struct-copy url pkg-url + [scheme "http"] + [path (list* user repo (path/param "tree" empty) branch path)]))] + [_ + pkg-url-str])) + +(define (page/info req pkg-name) + (page/info-like + (list (cons "Packages" (main-url page/main)) + pkg-name) + #f + (λ (embed/url t) + (main-url page/search (list t))) + req pkg-name)) + +(define (search-term-eval pkg-name info term) + (match term + [(regexp #rx"^author:(.*?)$" (list _ author)) + (equal? author (package-ref info 'author))] + [_ + (define term-rx (regexp-quote term)) + (for/or ([tag (list* pkg-name (package-ref info 'tags))]) + (regexp-match? term-rx tag))])) + +(define breadcrumb->string + (match-lambda + [(? string? label) + label] + [(cons (? string? label) + (? string? url)) + label])) +(define breadcrumb->xexpr + (match-lambda + [(? string? label) + `(span ,label)] + [(cons (? string? label) + (? string? url)) + `(span (a ([href ,url]) ,label))])) + +(define (template req #:breadcrumb bc . xexpr-forest) + (send/back + (response/xexpr + `(html + (head + (script ([src "/sorttable.js"]) " ") + (link ([rel "stylesheet"] + [type "text/css"] + [href "/style.css"])) + (title ,@(add-between (map breadcrumb->string bc) " > "))) + (body + (div ([class "breadcrumb"]) + ,@(add-between (map breadcrumb->xexpr bc) " > ") + ,(cond + [(current-user req #f) + => (λ (user) + `(span ([id "logout"]) + ,user + " | " + (a ([href ,(main-url page/logout)]) "logout")))] + [else + ""])) + ,@xexpr-forest + (div ([id "footer"]) + "Powered by " + (a ([href "http://racket-lang.org/"]) "Racket") ". " + "Written by " + (a ([href "http://faculty.cs.byu.edu/~jay"]) "Jay McCarthy") + ".")))))) + +(define (page/logout req) + (redirect-to + (main-url page/main) + #:headers + (list (cookie->header (logout-id-cookie id-cookie-name))))) + +(define (package-list/search ts) + (filter + (λ (p) + (define i (package-info p)) + (for/and ([t (in-list ts)]) + (search-term-eval p i t))) + (package-list))) + +(define search-formlet + (formlet + ,{(to-string (required (text-input))) + . => . new-terms} + (string-split new-terms))) + +(define (page/search/query req old-terms) + (define terms (formlet-process search-formlet req)) + (redirect-to (main-url page/search (append old-terms terms)))) + +(define (page/search req terms) + (define pkgs (package-list/search terms)) + (template + req + #:breadcrumb + (list* (cons "Packages" (main-url page/main)) + "Search" + (for/list ([t (in-list terms)]) + (cons t (main-url page/search (remove* (list t) terms))))) + `(div ([id "menu"]) + (form ([action ,(main-url page/search/query terms)]) + (span ([class "menu_option"]) + ,@(formlet-display search-formlet) + (input ([type "submit"] [value "Search"]))) + (span ([class "menu_option"]) + (a ([href ,(main-url page/manage)]) + ,(if (current-user req #f) + "Manage Your Packages" + "Contribute a Package"))))) + (package-table page/info pkgs #:terms terms))) + +(define (page/login req) + (login req) + (redirect-to (main-url page/main))) + +(define (login req [last-error #f]) + (define login-formlet + (formlet + (table + (tr (td "Email Address:") + (td ,{(to-string (required (text-input))) . => . email})) + (tr (td "Password:") + (td ,{(to-string (required (password-input))) . => . passwd}))) + (values email passwd))) + (define log-req + (send/suspend + (λ (k-url) + (template + req + #:breadcrumb + (list "Login") + `(div ([id "login"]) + (form ([action ,k-url] [method "post"]) + ,@(formlet-display login-formlet) + (input ([type "submit"] [value "Log in"]))) + (p "If you enter an unclaimed email address, then an account will be created.") + (p "Passwords are stored in the delicious SHA1 format, but transfered as plain-text over the HTTPS connection.") + ,@(if last-error + `((h1 ([class "error"]) ,last-error)) + '())))))) + (define-values + (email passwd) + (formlet-process login-formlet log-req)) + + (define (authenticated!) + (redirect/get + #:headers + (list + (cookie->header + (make-id-cookie id-cookie-name secret-key email))))) + + (when (regexp-match (regexp-quote "/") email) + (send/back + (template + log-req + #:breadcrumb + (list "Login" "Account Registration Error") + `(p "Email addresses may not contain / on Planet2:" + (tt ,email))))) + + (define password-path (build-path users-path email)) + + (cond + [(not (file-exists? password-path)) + (send/suspend + (λ (k-url) + (send-mail-message + "planet2@racket-lang.org" + "Account confirmation for Planet2" + (list email) + empty empty + (list "Someone tried to register your email address for an account on Planet2. If you want to authorize this registration and log in, please click the following link:" + "" + (format "https://plt-etc.byu.edu:9004~a" k-url) + "" + "This link will expire, so if it is not available, you'll have to try to register again.")) + (template + log-req + #:breadcrumb + (list "Login" "Account Registration") + `(p "An email has been sent to " + (tt ,email) + ", please click the link it contains to register and log in.")))) + (display-to-file (salty passwd) password-path) + (authenticated!)] + [(not (bytes=? (string->bytes/utf-8 (salty passwd)) + (file->bytes password-path))) + (login req (format "The given password is incorrect for email address ~e" + email))] + [else + (authenticated!)])) + +(define (current-user req required?) + (define id + (request-id-cookie id-cookie-name secret-key req)) + (cond + [id + id] + [required? + (current-user (login req) required?)] + [else + #f])) + +(define (package-list/mine req) + (define u (current-user req #t)) + (package-list/search (list (format "author:~a" u)))) + +(define (package-table page/package pkgs + #:terms [terms empty]) + `(table + ([class "packages sortable"]) + (thead + (tr (th "Package") (th "Author") (th "Description") (th "Tags"))) + (tbody + ,@(for/list ([p (in-list pkgs)]) + (define i (package-info p)) + (define author (package-ref i 'author)) + `(tr + ([class ,(if (< (- (current-seconds) (* 2 24 60 60)) + (package-ref i 'last-updated)) + "recent" + "")]) + (td (a ([href ,(main-url page/package p)]) + ,p)) + (td (a ([href ,(main-url page/search + (snoc terms + (format "author:~a" author)))]) + ,author)) + (td ,(package-ref i 'description)) + (td ,@(for/list ([t (in-list (package-ref i 'tags))]) + `(span (a ([href ,(main-url page/search (snoc terms t))]) + ,t) + " ")))))))) + +(define (page/manage req) + (define pkgs (package-list/mine req)) + (template + req + #:breadcrumb + (list (cons "Packages" (main-url page/main)) + (current-user req #t) + "Manage") + `(div ([id "menu"]) + (span ([class "menu_option"]) + (a ([href ,(main-url page/manage/upload)]) + "Upload a new package")) + (span ([class "menu_option"]) + (a ([href ,(main-url page/manage/update)]) + "Update checksums"))) + (package-table page/manage/edit pkgs))) + +(define (page/manage/upload req) + (page/manage/edit req #f)) + +(define (request-binding/string req id [fail? #t]) + (define res + (bindings-assq (string->bytes/utf-8 id) + (request-bindings/raw req))) + (cond + [res + (bytes->string/utf-8 + (binding:form-value + res))] + [fail? + (error 'planet2 "Missing field ~e" id)] + [else + #f])) + +(define (page/manage/edit req pkg) + (define (edit-details pkg-req) + (define new-pkg (request-binding/string pkg-req "name")) + (when (string=? new-pkg "") + (error 'planet2 "Name must not be empty: ~e" new-pkg)) + (define new-source (request-binding/string pkg-req "source")) + (when (string=? new-source "") + (error 'planet2 "Source must not be empty: ~e" new-source)) + (define new-desc (request-binding/string pkg-req "description")) + + (when (regexp-match #rx"[^a-zA-Z0-9_\\-]" new-pkg) + (error 'planet2 + "Illegal character in name; only alphanumerics, plus '-' and '_' allowed: ~e" + new-pkg)) + + (when (and (not (equal? pkg new-pkg)) + (or (regexp-match #rx"^[Pp][Ll][Tt]" new-pkg) + (regexp-match #rx"^[Pp][Ll][Aa][Nn][Ee][Tt]" new-pkg) + (regexp-match #rx"^[Rr][Aa][Cc][Kk][Ee][Tt]" new-pkg))) + (error 'planet2 + "Packages that start with plt, planet, and racket are not allowed without special permission. Please create your package with a different name, then email curation to request a rename: ~e" + new-pkg)) + + (when (and (package-exists? new-pkg) + (not (equal? (package-ref (package-info new-pkg) 'author) + (current-user pkg-req #t)))) + (error 'planet2 + "Packages may only be modified by their authors: ~e" + new-pkg)) + + (package-begin + (define* i + (if pkg + (package-info pkg) + (hasheq))) + + (define* i + (hash-set i 'name new-pkg)) + (define* i + (hash-set i 'source new-source)) + (define* i + (hash-set i 'author (current-user pkg-req #t))) + (define* i + (hash-set i 'description new-desc)) + (define* i + (hash-set i 'last-edit (current-seconds))) + (define* i + (if pkg + i + (hash-set i 'checksum ""))) + + (package-info-set! new-pkg i)) + + (unless (or (not pkg) (equal? new-pkg pkg)) + (package-remove! pkg)) + + (update-checksum new-pkg) + + (define new-tag + (request-binding/string pkg-req "tag" #f)) + (add-tag! new-pkg new-tag) + + (redirect-to + (main-url page/manage/edit new-pkg))) + + (page/info-like + (list* (cons "Packages" (main-url page/main)) + (current-user req #t) + (cons "Manage" (main-url page/manage)) + (if pkg + (list pkg + "Edit") + (list "Upload"))) + edit-details + (λ (embed/url t) + (embed/url (remove-tag-handler pkg t))) + req pkg)) + + +(define (tags-normalize ts) + (remove-duplicates (sort ts string-ciuseful-url (package-ref i 'source))]) + ,(package-ref i 'source))))) + (tr + (td "Checksum") + (td ,(package-ref* i 'checksum ""))) + (tr + (td "Last Update") + (td ,(format-time (package-ref* i 'last-updated #f)))) + (tr + (td "Last Checked") + (td ,(format-time (package-ref* i 'last-checked #f)))) + (tr + (td "Description") + (td ,(if edit-details + `(textarea ([name "description"]) + ,(package-ref* i 'description "")) + (package-ref i 'description)))) + (tr + (td "Last Edit") + (td ,(format-time (package-ref* i 'last-edit #f)))) + (tr + (td "Tags") + (td + (ul + ,@(for/list ([t (in-list (package-ref* i 'tags empty))]) + `(li (a ([href ,(tag-url embed/url t)]) + ,t))) + ,(if pkg-name + `(li (input ([name "tag"] [type "text"]))) + "")))) + `(tr (td ([class "submit"] [colspan "2"]) + (input ([type "submit"] [value "Submit"])))))) + (template + req + #:breadcrumb + bc + `(div + ([class "package"]) + (form ([action ,(embed/url form-handler)] [method "post"]) + ,the-table)))))) + +(define (page/manage/update req) + (update-checksums + (package-list/mine req)) + (redirect-to (main-url page/manage))) + +(define (update-checksums pkgs) + (for-each update-checksum pkgs)) + +(define (update-checksum pkg-name) + (define i (package-info pkg-name)) + (define old-checksum + (package-ref i 'checksum)) + (define now (current-seconds)) + (define new-checksum + (package-url->checksum (package-ref i 'source))) + (package-begin + (define* i + (hash-set i 'checksum + (or new-checksum + old-checksum))) + (define* i + (hash-set i 'last-checked now)) + (define* i + (if (and new-checksum (equal? new-checksum old-checksum)) + i + (hash-set i 'last-updated now))) + (package-info-set! pkg-name i))) + +(define basic-start + (planet2-index/basic package-list package-info)) + +(define (go port) + (printf "launching on port ~a\n" port) + (thread + (λ () + (while true + (printf "updating checksums\n") + (update-checksums (package-list)) + ;; update once per day based on whenever the server started + (sleep (* 24 60 60))))) + (serve/servlet + main-dispatch + #:command-line? #t + #:listen-ip #f + #:ssl? #t + #:ssl-cert (build-path root "server-cert.pem") + #:ssl-key (build-path root "private-key.pem") + #:extra-files-paths + (list (build-path src "static") + (build-path root "static")) + #:servlet-regexp #rx"" + #:port port)) + +(module+ main + (go 9004)) diff --git a/collects/meta/planet2-index/official/static/sorttable.js b/collects/meta/planet2-index/official/static/sorttable.js new file mode 100644 index 0000000000..4f74f1e2ea --- /dev/null +++ b/collects/meta/planet2-index/official/static/sorttable.js @@ -0,0 +1,515 @@ +function TocviewToggle(glyphid, id) { + var glyph = document.getElementById(glyphid); + var s = document.getElementById(id).style; + var expand = s.display == "none"; + s.display = expand ? "block" : "none"; + glyph.innerHTML = expand ? "▼" : "►"; +} + +function ToggleOn(id) { + var s = document.getElementById(id).style; + var li = document.getElementById("li" + id); + s.display = "block"; + li.setAttribute("class", "tab-selected"); +} +function ToggleOff(id) { + var s = document.getElementById(id).style; + var li = document.getElementById("li" + id); + s.display = "none"; + li.setAttribute("class", ""); +} + +/* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. +*/ + + +var stIsIE = /*@cc_on!@*/false; + +sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + makeSortable: function(table) { + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backwards compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i5' : ' ▴'; + this.appendChild(sortrevind); + return; + } + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted_reverse', + 'sorttable_sorted'); + this.removeChild(document.getElementById('sorttable_sortrevind')); + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + return; + } + + // remove sorttable_sorted classes + theadrow = this.parentNode; + forEach(theadrow.childNodes, function(cell) { + if (cell.nodeType == 1) { // an element + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + }); + sortfwdind = document.getElementById('sorttable_sortfwdind'); + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } + sortrevind = document.getElementById('sorttable_sortrevind'); + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } + + this.className += ' sorttable_sorted'; + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + row_array = []; + col = this.sorttable_columnindex; + rows = this.sorttable_tbody.rows; + for (var j=0; j 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for fields. + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + reverse: function(tbody) { + // reverse the rows in a tbody + newrows = []; + for (var i=0; i=0; i--) { + tbody.appendChild(newrows[i]); + } + delete newrows; + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a[0] and b[0] */ + sort_numeric: function(a,b) { + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + sort_alpha: function(a,b) { + if (a[0]==b[0]) return 0; + if (a[0] 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } +} + +/* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + +// Dean Edwards/Matthias Miller/John Resig + +/* for Mozilla/Opera9 */ +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); +} + +/* for Internet Explorer */ +/*@cc_on @*/ +/*@if (@_win32) + document.write("