Example #1
0
 def post(self):
     """Perform an action."""
     form = web.form("action", channel="default")
     response = {}
     if form.action == "channels":
         web.tx.sub.add_channel(form.name)
         response = {"channels": web.tx.sub.get_channels()}
     elif form.action == "search":
         response = {"results": web.tx.sub.search(form.query)}
     elif form.action == "preview":
         response = web.tx.sub.preview(form.url)
     elif form.action == "follow":
         web.tx.sub.follow(form.url)
         response = {"items": web.tx.sub.get_following()}
         web.enqueue(websub.subscribe,
                     f"{web.tx.origin}/subscriptions/sent", form.url)
     elif form.action == "unfollow":
         pass
     elif form.action == "timeline":
         pass
     elif form.action == "mute":
         pass
     elif form.action == "unmute":
         pass
     elif form.action == "block":
         pass
     elif form.action == "unblock":
         pass
     web.header("Content-Type", "application/json")
     return response
Example #2
0
 def options(self):
     """Signal capabilities to CardDAV client."""
     web.header("DAV", "1, 2, 3, access-control, addressbook")
     web.header(
         "Allow",
         "OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, "
         "COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, "
         "UNLOCK, REPORT, ACL",
     )
     web.tx.response.naked = True
     return ""
Example #3
0
 def get(self):
     """Return a list of tokens to owner otherwise a form to submit a code."""
     # TODO move to library?
     try:
         auth = tx.auth_server.get_auth_from_token(
             str(tx.request.headers["authorization"]))
     except IndexError:
         raise web.Forbidden("token could not be found")
     web.header("Content-Type", "application/json")
     return {
         "me": auth["response"]["me"],
         "client_id": auth["client_id"],
         "scope": " ".join(auth["response"]["scope"]),
     }
Example #4
0
def redeem_authorization_code(flow: str,
                              me: web.uri,
                              name: str = None,
                              email: str = None,
                              photo: web.uri = None) -> dict:
    """
    Redeem an authorization code with given `flow` and return a profile and/or a token.

    `flow` can be one of ['profile'][0] or ['token'][1].

    [0]: https://indieauth.spec.indieweb.org/#profile-url-response
    [1]: https://indieauth.spec.indieweb.org/#access-token-response

    """
    form = web.form("code",
                    "client_id",
                    "redirect_uri",
                    grant_type="authorization_code")
    # TODO verify authenticity
    # TODO grant_type=refresh_token
    if form.grant_type not in ("authorization_code", "refresh_token"):
        raise web.Forbidden(f"`grant_type` {form.grant_type} not supported")
    auth = tx.auth_server.get_auth_from_code(form.code)
    if form.client_id != auth["client_id"]:
        raise web.BadRequest("`client_id` does not match original request")
    if form.redirect_uri != auth["redirect_uri"]:
        raise web.BadRequest("`redirect_uri` does not match original request")
    if "code_verifier" in form:
        if not auth["code_challenge"]:
            raise web.BadRequest("`code_verifier` without a `code_challenge`")
        if auth["code_challenge"] != generate_challenge(form.code_verifier):
            raise web.Forbidden("code mismatch")
    elif auth["code_challenge"]:
        raise web.BadRequest("`code_challenge` without `code_verifier`")
    response = auth["response"]
    if flow == "token":
        if not response["scope"]:
            raise web.BadRequest("Access Token request requires a scope")
        response.update(
            token_type="Bearer",
            access_token=f"secret-token:{web.nbrandom(24)}",
        )
    response["me"] = me
    if "profile" in response["scope"]:
        response["profile"] = {"url": me, "name": name, "photo": photo}
        if "email" in response["scope"] and email:
            response["profile"]["email"] = email
    tx.auth_server.update_auth(response, auth["code"])
    web.header("Content-Type", "application/json")
    return response
Example #5
0
 def get(self):
     """Perform an action or return an activity summary."""
     try:
         form = web.form("action", channel="default")
     except web.BadRequest:
         return app.view.activity(web.tx.sub.get_following(),
                                  web.tx.sub.get_channels())
     response = {}
     if form.action == "channels":
         response = {"channels": web.tx.sub.get_channels()}
     if form.action == "follow":
         response = {"items": web.tx.sub.get_following()}
     elif form.action == "timeline":
         response = {"items": []}
     web.header("Content-Type", "application/json")
     return response
Example #6
0
def wrap(handler, main_app):
    """Ensure an owner exists and then add their details to the transaction."""
    web.tx.response.claimed = True
    try:
        web.tx.host.owner = web.tx.identities.get_identity(
            web.tx.origin)["card"]
    except IndexError:
        web.header("Content-Type", "text/html")
        # if web.tx.request.method == "GET":
        #     web.tx.response.claimed = False
        #     raise web.NotFound(app.view.claim())
        # elif web.tx.request.method == "POST":
        # name = web.form("name").name
        web.tx.identities.add_identity(web.tx.origin, "Anonymous")
        passphrase = " ".join(web.tx.identities.add_passphrase())
        web.tx.host.owner = web.tx.user.session = web.tx.identities.get_identity(
            web.tx.origin)["card"]
        web.tx.user.is_owner = True
        if kiosk := web.form(kiosk=None).kiosk:
            with open(f"{kiosk}/passphrase", "w") as fp:
                fp.write(passphrase)
            raise web.SeeOther("/")
        raise web.Created(app.view.claimed(web.tx.origin, passphrase),
                          web.tx.origin)
Example #7
0
    def put(self):
        """
        add or update a identity

        """
        # TODO only add if "if-none-match" is found and identity isn't
        try:
            print("if-none-match", web.tx.request.headers.if_none_match)
        except AttributeError:
            pass
        else:
            try:
                identities.get_identity_by_uuid(self.card_id)
            except ResourceNotFound:
                pass
            else:
                raise web.Conflict("identity already exists")

        # TODO only update if "if-match" matches etag on hand
        try:
            request_etag = str(web.tx.request.headers.if_match).strip('"')
            print("if-match", request_etag)
        except AttributeError:
            pass
        else:
            identity = identities.get_identity_by_uuid(self.card_id)
            current_etag = identity.get("updated",
                                        identity["published"]).timestamp()
            print("current etag", current_etag)
            if request_etag != current_etag:
                raise web.Conflict("previous edit already exists")

        # TODO non-standard type-params (url) not handled by vobject

        card = vobject.readOne(web.tx.request.body.decode("utf-8"))

        name = card.fn.value.strip()

        extended = {}
        n = card.n.value

        def explode(key):
            item = getattr(n, key)
            if isinstance(item, list):
                extended[key] = ";".join(item)
            else:
                extended[key] = [item]

        explode("prefix")
        explode("given")
        explode("additional")
        explode("family")
        explode("suffix")

        # TODO identity_type = "identity"
        basic = {"name": name, "uuid": self.card_id}

        # TODO organizations = [o.value[0]
        # TODO                  for o in card.contents.get("org", [])]
        # TODO for organization in organizations:
        # TODO     if organization == name:
        # TODO         identity_type = "organization"

        # TODO telephones = []
        # TODO for tel in card.contents.get("tel", []):
        # TODO     telephones.append((tel.value, tel.params["TYPE"]))
        # TODO websites = []
        # TODO for url in card.contents.get("url", []):
        # TODO     type = url.params.get("TYPE", [])
        # TODO     for label in card.contents.get("x-ablabel"):
        # TODO         if label.group == url.group:
        # TODO             type.append(label.value)
        # TODO     print(url.value, type)
        # TODO     print()
        # TODO     websites.append((url.value, type))

        # photo = card.contents.get("photo")[0]
        # print()
        # print(photo)
        # print()
        # print(photo.group)
        # print(photo.params.get("ENCODING"))
        # print(photo.params.get("X-ABCROP-RECTANGLE"))
        # print(photo.params.get("TYPE", []))
        # print(len(photo.value))
        # print()
        # filepath = tempfile.mkstemp()[1]
        # with open(filepath, "wb") as fp:
        #     fp.write(photo.value)
        # photo_id = canopy.branches["images"].photos.upload(filepath)
        # extended["photos"] = [photo_id]

        try:
            details = identities.get_identity_by_uuid(self.card_id)
        except ResourceNotFound:
            print("NEW identity!")
            print(basic)
            print(extended)
            quick_draft("identity",
                        basic,
                        publish="Identity imported from iPhone.")
            # XXX details = create_identity(access="private", uid=self.card_id,
            # XXX                          **basic)
            # XXX details = update_identity(identifier=details["identifier"],
            # XXX                  telephones=telephones, websites=websites,
            # XXX                  **extended)
            print("CREATED")
        else:
            print("EXISTING identity!")
            print(details)
            print("UPDATED")
        # XXX     basic.update(extended)
        # XXX     details = update_identity(identifier=details["identifier"],
        # XXX                      telephones=telephones, websites=websites,
        # XXX                      **basic)
        identity = identities.get_identity_by_uuid(self.card_id)
        etag = identity.get("updated", identity["published"]).timestamp()
        web.header("ETag", f'"{etag}"')
        web.tx.response.naked = True
        raise web.Created("created identity",
                          f"/identities/{self.card_id}.vcf")
Example #8
0
 def get(self):
     """"""
     web.header("Content-Type", "text/vcard")
     return generate_vcard(self.nickname)
Example #9
0
    def propfind(self):
        """
        Return a status listing of addressbook/contacts.

        This resource is requsted twice with `Depth` headers of 0 and 1.
        0 is a request for the addressbook itself. 1 is a request for the
        addressbook itself and all contacts in the addressbook. Thus both
        the addressbook itself and each user have an etag.

        """
        # TODO refactor..
        web.header("DAV", "1, 2, 3, access-control, addressbook")

        depth = int(web.tx.request.headers["Depth"])
        etags = {"": web.tx.kv["carddav-lasttouch"]}
        if depth == 1:
            for identity in get_resources("identities"):
                etags[identity["-uuid"]] = identity.get(
                    "updated", identity["published"]).timestamp()

        props = list(web.tx.request.body.iterchildren())[0]
        namespaces = set()
        responses = []

        for uuid, etag in etags.items():
            ok = []
            notfound = []
            for prop in props.iterchildren():
                # supported
                if prop.tag == "{DAV:}current-user-privilege-set":
                    ok.append("""<current-user-privilege-set>
                                     <privilege>
                                         <all />
                                         <read />
                                         <write />
                                         <write-properties />
                                         <write-content />
                                     </privilege>
                                 </current-user-privilege-set>""")
                if prop.tag == "{DAV:}displayname":
                    ok.append("<displayname>carddav</displayname>")
                if prop.tag == "{DAV:}getetag":
                    ok.append(f'<getetag>"{etag}"</getetag>')
                if prop.tag == "{DAV:}owner":
                    ok.append("<owner>/</owner>")
                if prop.tag == "{DAV:}principal-URL":
                    ok.append("""<principal-URL>
                                     <href>/identities</href>
                                 </principal-URL>""")
                if prop.tag == "{DAV:}principal-collection-set":
                    ok.append("""<principal-collection-set>
                                     <href>/identities</href>
                                 </principal-collection-set>""")
                if prop.tag == "{DAV:}current-user-principal":
                    ok.append("""<current-user-principal>
                                     <href>/identities</href>
                                 </current-user-principal>""")
                if prop.tag == "{DAV:}resourcetype":
                    namespaces.add("CR")
                    if uuid:
                        ok.append("<resourcetype />")
                    else:
                        ok.append("""<resourcetype>
                                         <CR:addressbook />
                                         <collection />
                                     </resourcetype>""")
                if prop.tag == "{DAV:}supported-report-set":
                    ok.append("""<supported-report-set>
                                     <supported-report>
                                         <report>principal-property-search</report>
                                     </supported-report>
                                     <supported-report>
                                         <report>sync-collection</report>
                                     </supported-report>
                                     <supported-report>
                                         <report>expand-property</report>
                                     </supported-report>
                                     <supported-report>
                                         <report>principal-search-property-set</report>
                                     </supported-report>
                                 </supported-report-set>""")
                if (prop.tag == "{urn:ietf:params:xml:ns:carddav}"
                        "addressbook-home-set"):
                    namespaces.add("CR")
                    ok.append("""<CR:addressbook-home-set>
                                     <href>/identities</href>
                                 </CR:addressbook-home-set>""")
                if prop.tag == "{http://calendarserver.org/ns/}" "getctag":
                    namespaces.add("CS")
                    ok.append(f'<CS:getctag>"{etag}"</CS:getctag>')

                # conditionally supported
                if prop.tag == "{http://calendarserver.org/ns/}me-card":
                    namespaces.add("CS")
                    if uuid:
                        notfound.append("<CS:me-card />")
                    else:
                        ok.append(f"""<CS:me-card>
                                      <href>/identities/{web.tx.owner["-uuid"]}.vcf</href>
                                      </CS:me-card>""")

                # not supported
                if prop.tag == "{DAV:}add-member":
                    notfound.append("<add-member />")
                if prop.tag == "{DAV:}quota-available-bytes":
                    notfound.append("<quota-available-bytes />")
                if prop.tag == "{DAV:}quota-used-bytes":
                    notfound.append("<quota-used-bytes />")
                if prop.tag == "{DAV:}resource-id":
                    notfound.append("<resource-id />")
                if prop.tag == "{DAV:}sync-token":
                    notfound.append("<sync-token />")
                if prop.tag == "{urn:ietf:params:xml:ns:carddav}" "directory-gateway":
                    namespaces.add("CR")
                    notfound.append("<CR:directory-gateway />")
                if prop.tag == "{urn:ietf:params:xml:ns:carddav}" "max-image-size":
                    namespaces.add("CR")
                    notfound.append("<CR:max-image-size />")
                if prop.tag == "{urn:ietf:params:xml:ns:carddav}" "max-resource-size":
                    namespaces.add("CR")
                    notfound.append("<CR:max-resource-size />")
                if prop.tag == "{http://calendarserver.org/ns/}" "email-address-set":
                    namespaces.add("CS")
                    notfound.append("<CS:email-address-set />")
                if prop.tag == "{http://calendarserver.org/ns/}" "push-transports":
                    namespaces.add("CS")
                    notfound.append("<CS:push-transports />")
                if prop.tag == "{http://calendarserver.org/ns/}" "pushkey":
                    namespaces.add("CS")
                    notfound.append("<CS:pushkey />")
                if prop.tag == "{http://me.com/_namespace/}" "bulk-requests":
                    namespaces.add("ME")
                    notfound.append("<ME:bulk-requests />")
            href = "/identities"
            if uuid:
                href += f"/{uuid}.vcf"
            responses.append((href, ok, notfound))
        web.tx.response.naked = True
        raise web.MultiStatus(view.carddav(namespaces, responses))