Ejemplo n.º 1
0
    def test_maybe_add_or_delete_source_delete_declined(self):
        state = {
            'feature': 'webmention',
            'operation': 'delete',
        }
        msg = 'If you want to disable, please approve the FakeSource prompt.'

        # no source
        with app.test_request_context():
            with self.assertRaises(RequestRedirect) as rr:
                util.maybe_add_or_delete_source(FakeSource, None,
                                                util.encode_oauth_state(state))

            self.assert_equals(302, rr.exception.code)
            self.assert_equals('http://localhost/', rr.exception.new_url)
            self.assertEqual([msg], get_flashed_messages())

        # source
        state['source'] = self.sources[0].key.urlsafe().decode()
        with app.test_request_context(), self.assertRaises(
                RequestRedirect) as rr:
            util.maybe_add_or_delete_source(FakeSource, None,
                                            util.encode_oauth_state(state))

            self.assert_equals(302, rr.exception.code)
            self.assert_equals(self.source_bridgy_url, rr.exception.new_url)
            self.assertEqual([msg], get_flashed_messages())
Ejemplo n.º 2
0
    def test_registration_api_start_handler_post(self):
        self.expect_site_fetch()
        self.mox.ReplayAll()
        resp = instagram.application.get_response('/instagram/start',
                                                  method='POST',
                                                  body=urllib.urlencode(
                                                      self.bridgy_api_state))

        self.assertEquals(302, resp.status_code)

        state_json = util.encode_oauth_state(self.bridgy_api_state)
        expected_auth_url = indieauth.INDIEAUTH_URL + '?' + urllib.urlencode({
            'me':
            'http://snarfed.org',
            'client_id':
            appengine_config.INDIEAUTH_CLIENT_ID,
            'redirect_uri':
            'http://localhost/instagram/callback',
            'state':
            util.encode_oauth_state({
                'endpoint': indieauth.INDIEAUTH_URL,
                'me': 'http://snarfed.org',
                'state': state_json,
            }),
        })
        self.assertEquals(expected_auth_url, resp.headers['Location'])
Ejemplo n.º 3
0
  def post(self):
    source = self.load_source(param='key')
    kind = source.key.kind()
    feature = util.get_required_param(self, 'feature')
    state = util.encode_oauth_state({
      'operation': 'delete',
      'feature': feature,
      'source': source.key.urlsafe().decode(),
      'callback': self.request.get('callback'),
    })

    # Blogger don't support redirect_url() yet
    if kind == 'Blogger':
      return self.redirect('/blogger/delete/start?state=%s' % state)

    path = ('/reddit/callback' if kind == 'Reddit'
            else '/wordpress/add' if kind == 'WordPress'
            else '/%s/delete/finish' % source.SHORT_NAME)
    kwargs = {}
    if kind == 'Twitter':
      kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

    handler = source.OAUTH_START_HANDLER.to(path, **kwargs)(self.request, self.response)
    try:
      self.redirect(handler.redirect_url(state=state))
    except Exception as e:
      code, body = util.interpret_http_exception(e)
      if not code and util.is_connection_failure(e):
        code = '-'
        body = str(e)
      if code:
        self.messages.add('%s API error %s: %s' % (source.GR_CLASS.NAME, code, body))
        self.redirect(source.bridgy_url(self))
      else:
        raise
Ejemplo n.º 4
0
  def post(self):
    source = self.load_source(param='key')
    module = self.OAUTH_MODULES[source.key.kind()]
    feature = util.get_required_param(self, 'feature')
    state = util.encode_oauth_state({
      'operation': 'delete',
      'feature': feature,
      'source': source.key.urlsafe(),
      'callback': self.request.get('callback'),
    })

    # Blogger don't support redirect_url() yet
    if module is oauth_blogger_v2:
      return self.redirect('/blogger/delete/start?state=%s' % state)

    path = ('/instagram/callback' if module is indieauth
            else '/wordpress/add' if module is oauth_wordpress_rest
            else '/%s/delete/finish' % source.SHORT_NAME)
    kwargs = {}
    if module is oauth_twitter:
      kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

    handler = module.StartHandler.to(path, **kwargs)(self.request, self.response)
    try:
      self.redirect(handler.redirect_url(state=state))
    except Exception as e:
      code, body = util.interpret_http_exception(e)
      if not code and util.is_connection_failure(e):
        code = '-'
        body = unicode(e)
      if code:
        self.messages.add('%s API error %s: %s' % (source.GR_CLASS.NAME, code, body))
        self.redirect(source.bridgy_url(self))
      else:
        raise
Ejemplo n.º 5
0
  def _render_preview(self, result, include_link=False):
    """Renders a preview CreationResult as HTML.

    Args:
      result: CreationResult
      include_link: boolean

    Returns: CreationResult with the rendered HTML in content
    """
    state = {
      'source_key': self.source.key.urlsafe().decode(),
      'source_url': self.source_url(),
      'target_url': self.target_url(),
      'include_link': include_link,
    }
    vars = {
      'source': self.preprocess_source(self.source),
      'preview': result.content,
      'description': result.description,
      'webmention_endpoint': util.host_url(self) + '/publish/webmention',
      'state': util.encode_oauth_state(state),
    }
    vars.update(state)
    logging.info('Rendering preview with template vars %s', pprint.pformat(vars))
    return gr_source.creation_result(
      JINJA_ENV.get_template('preview.html').render(**vars))
Ejemplo n.º 6
0
  def _render_preview(self, result, include_link=False):
    """Renders a preview CreationResult as HTML.

    Args:
      result: CreationResult
      include_link: boolean

    Returns: CreationResult with the rendered HTML in content
    """
    state = {
      'source_key': self.source.key.urlsafe(),
      'source_url': self.source_url(),
      'target_url': self.target_url(),
      'include_link': include_link,
    }
    vars = {
      'source': self.preprocess_source(self.source),
      'preview': result.content,
      'description': result.description,
      'webmention_endpoint': util.host_url(self) + '/publish/webmention',
      'state': util.encode_oauth_state(state),
    }
    vars.update(state)
    logging.info('Rendering preview with template vars %s', pprint.pformat(vars))
    return gr_source.creation_result(
      JINJA_ENV.get_template('preview.html').render(**vars))
Ejemplo n.º 7
0
 def callback(self, state=''):
   resp = instagram.application.get_response(
     '/instagram/callback?code=my_code&state=%s' % util.encode_oauth_state({
       'endpoint': indieauth.INDIEAUTH_URL,
       'me': 'http://snarfed.org',
       'state': state,
     }))
   self.assertEquals(302, resp.status_int)
   return resp
Ejemplo n.º 8
0
 def callback(self, token='towkin'):
     resp = app.application.get_response(
         '/indieauth/callback?code=my_code&state=%s' %
         util.encode_oauth_state({
             'endpoint': indieauth.INDIEAUTH_URL,
             'me': 'http://snarfed.org',
             'state': token,
         }))
     self.assertEqual(302, resp.status_int)
     return resp
Ejemplo n.º 9
0
 def callback(self, token='towkin'):
     state = util.encode_oauth_state({
         'endpoint': indieauth.INDIEAUTH_URL,
         'me': 'http://snarfed.org',
         'state': token,
     })
     resp = self.client.get(
         f'/indieauth/callback?code=my_code&state={state}')
     self.assertEqual(302, resp.status_code)
     return resp
Ejemplo n.º 10
0
  def test_registration_api_finish_no_rel_me(self):
    state = util.encode_oauth_state(self.bridgy_api_state)
    self.expect_indieauth_check(state=state)
    self.expect_site_fetch('')

    self.mox.ReplayAll()
    resp = self.callback(state=urllib.quote_plus(state))
    self.assertEquals(302, resp.status_int)
    location = urllib.unquote_plus(resp.headers['Location'])
    self.assertTrue(location.startswith(
      'http://localhost/#!No Instagram profile found.'), location)
Ejemplo n.º 11
0
  def test_registration_api_start_handler_post(self):
    self.expect_site_fetch()
    self.mox.ReplayAll()
    resp = instagram.application.get_response(
      '/instagram/start', method='POST', body=urllib.urlencode(self.bridgy_api_state))

    self.assertEquals(302, resp.status_code)

    state_json = util.encode_oauth_state(self.bridgy_api_state)
    expected_auth_url = indieauth.INDIEAUTH_URL + '?' + urllib.urlencode({
      'me': 'http://snarfed.org',
      'client_id': appengine_config.INDIEAUTH_CLIENT_ID,
      'redirect_uri': 'http://localhost/instagram/callback',
      'state': util.encode_oauth_state({
        'endpoint': indieauth.INDIEAUTH_URL,
        'me': 'http://snarfed.org',
        'state': state_json,
      }),
    })
    self.assertEquals(expected_auth_url, resp.headers['Location'])
Ejemplo n.º 12
0
  def test_maybe_add_or_delete_source_delete_declined(self):
    state = {
      'feature': 'webmention',
      'operation': 'delete',
    }
    msg = '#!If%20you%20want%20to%20disable%2C%20please%20approve%20the%20FakeSource%20prompt.'

    # no source
    self.assertIsNone(self.handler.maybe_add_or_delete_source(
      FakeSource, None, util.encode_oauth_state(state)))
    self.assert_equals(302, self.handler.response.status_code)
    self.assert_equals('http://localhost/' + msg,
                       self.handler.response.headers['location'])

    # source
    state['source'] = self.sources[0].key.urlsafe().decode()
    self.assertIsNone(self.handler.maybe_add_or_delete_source(
      FakeSource, None, util.encode_oauth_state(state)))
    self.assert_equals(302, self.handler.response.status_code)
    self.assert_equals(self.sources[0].bridgy_url(self.handler) + msg,
                       self.handler.response.headers['location'])
Ejemplo n.º 13
0
  def test_maybe_add_or_delete_source_delete_declined(self):
    state = {
      'feature': 'webmention',
      'operation': 'delete',
    }
    msg = '#!If%20you%20want%20to%20disable%2C%20please%20approve%20the%20FakeSource%20prompt.'

    # no source
    self.assertIsNone(self.handler.maybe_add_or_delete_source(
      FakeSource, None, util.encode_oauth_state(state)))
    self.assert_equals(302, self.handler.response.status_code)
    self.assert_equals('http://localhost/' + msg,
                       self.handler.response.headers['location'])

    # source
    state['source'] = self.sources[0].key.urlsafe()
    self.assertIsNone(self.handler.maybe_add_or_delete_source(
      FakeSource, None, util.encode_oauth_state(state)))
    self.assert_equals(302, self.handler.response.status_code)
    self.assert_equals(self.sources[0].bridgy_url(self.handler) + msg,
                       self.handler.response.headers['location'])
Ejemplo n.º 14
0
  def test_registration_api_finish_success(self):
    state = util.encode_oauth_state(self.bridgy_api_state)
    self.expect_indieauth_check(state=state)
    self.expect_site_fetch()
    self.expect_instagram_fetch()
    self.expect_webmention_discovery()

    self.mox.ReplayAll()
    resp = self.callback(state=urllib.quote_plus(state))
    self.assertEquals(302, resp.status_int)
    self.assertEquals('http://my.site/call-back?' + urllib.urlencode({
      'result': 'success',
      'key': self.inst.key.urlsafe(),
      'user': '******',
    }), resp.headers['Location'])
Ejemplo n.º 15
0
    def post(self):
        key = ndb.Key(urlsafe=util.get_required_param(self, 'key'))
        module = self.OAUTH_MODULES[key.kind()]
        feature = util.get_required_param(self, 'feature')
        state = util.encode_oauth_state({
            'operation':
            'delete',
            'feature':
            feature,
            'source':
            key.urlsafe(),
            'callback':
            self.request.get('callback'),
        })

        # Google+ and Blogger don't support redirect_url() yet
        if module is oauth_googleplus:
            return self.redirect('/googleplus/delete/start?state=%s' % state)

        if module is oauth_blogger_v2:
            return self.redirect('/blogger/delete/start?state=%s' % state)

        source = key.get()
        path = ('/instagram/callback' if module is indieauth else
                '/wordpress/add' if module is oauth_wordpress_rest else
                '/%s/delete/finish' % source.SHORT_NAME)
        kwargs = {}
        if module is oauth_twitter:
            kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

        handler = module.StartHandler.to(path, **kwargs)(self.request,
                                                         self.response)
        try:
            self.redirect(handler.redirect_url(state=state))
        except Exception as e:
            code, body = util.interpret_http_exception(e)
            if not code and util.is_connection_failure(e):
                code = '-'
                body = unicode(e)
            if code:
                self.messages.add('%s API error %s: %s' %
                                  (source.GR_CLASS.NAME, code, body))
                self.redirect(source.bridgy_url(self))
            else:
                raise
Ejemplo n.º 16
0
def delete_start():
    source = util.load_source()
    kind = source.key.kind()
    feature = request.form['feature']
    state = util.encode_oauth_state({
        'operation': 'delete',
        'feature': feature,
        'source': source.key.urlsafe().decode(),
        'callback': request.values.get('callback'),
    })

    # Blogger don't support redirect_url() yet
    if kind == 'Blogger':
        return redirect(f'/blogger/delete/start?state={state}')

    path = ('/reddit/callback' if kind == 'Reddit' else '/wordpress/add'
            if kind == 'WordPress' else f'/{source.SHORT_NAME}/delete/finish')
    kwargs = {}
    if kind == 'Twitter':
        kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

    try:
        return redirect(source.OAUTH_START(path).redirect_url(state=state))
    except werkzeug.exceptions.HTTPException:
        # raised by us, probably via self.error()
        raise
    except Exception as e:
        code, body = util.interpret_http_exception(e)
        if not code and util.is_connection_failure(e):
            code = '-'
            body = str(e)
        if code:
            flash(f'{source.GR_CLASS.NAME} API error {code}: {body}')
            return redirect(source.bridgy_url())
        else:
            raise
Ejemplo n.º 17
0
  def attempt_single_item(self, item):
    """Attempts to preview or publish a single mf2 item.

    Args:
      item: mf2 item dict from mf2py

    Returns:
      CreationResult
    """
    self.maybe_inject_silo_content(item)
    obj = microformats2.json_to_object(item)

    ignore_formatting = self.ignore_formatting(item)
    if ignore_formatting:
      prop = microformats2.first_props(item.get('properties', {}))
      content = microformats2.get_text(prop.get('content'))
      if content:
        obj['content'] = content.strip()

    # which original post URL to include? in order of preference:
    # 1. rel-shortlink (background: https://github.com/snarfed/bridgy/issues/173)
    # 2. original user-provided URL if it redirected
    # 3. u-url if available
    # 4. actual final fetched URL
    if self.shortlink:
      obj['url'] = self.shortlink
    elif self.source_url() != self.fetched.url:
      obj['url'] = self.source_url()
    elif 'url' not in obj:
      obj['url'] = self.fetched.url
    logging.debug('Converted to ActivityStreams object: %s', json.dumps(obj, indent=2))

    # posts and comments need content
    obj_type = obj.get('objectType')
    if obj_type in ('note', 'article', 'comment'):
      if (not obj.get('content') and not obj.get('summary') and
          not obj.get('displayName')):
        return gr_source.creation_result(
          abort=False,
          error_plain='Could not find content in %s' % self.fetched.url,
          error_html='Could not find <a href="http://microformats.org/">content</a> in %s' % self.fetched.url)

    self.preprocess(obj)

    include_link = self.include_link(item)

    if not self.authorize():
      return gr_source.creation_result(abort=True)

    # RIP Facebook comments/likes. https://github.com/snarfed/bridgy/issues/350
    if (isinstance(self.source, FacebookPage) and
        (obj_type == 'comment' or obj.get('verb') == 'like')):
      return gr_source.creation_result(
        abort=True,
        error_plain='Facebook comments and likes are no longer supported. :(',
        error_html='<a href="https://github.com/snarfed/bridgy/issues/350">'
                   'Facebook comments and likes are no longer supported.</a> :(')

    if self.PREVIEW:
      result = self.source.gr_source.preview_create(
        obj, include_link=include_link, ignore_formatting=ignore_formatting)
      self.entity.published = result.content or result.description
      if not self.entity.published:
        return result  # there was an error
      state = {
        'source_key': self.source.key.urlsafe(),
        'source_url': self.source_url(),
        'target_url': self.target_url(),
        'include_link': include_link,
      }
      vars = {'source': self.preprocess_source(self.source),
              'preview': result.content,
              'description': result.description,
              'webmention_endpoint': self.request.host_url + '/publish/webmention',
              'state': util.encode_oauth_state(state),
              }
      vars.update(state)
      logging.info('Rendering preview with template vars %s', pprint.pformat(vars))
      return gr_source.creation_result(
        JINJA_ENV.get_template('preview.html').render(**vars))

    else:
      result = self.source.gr_source.create(
        obj, include_link=include_link, ignore_formatting=ignore_formatting)
      self.entity.published = result.content
      if not result.content:
        return result  # there was an error
      if 'url' not in self.entity.published:
        self.entity.published['url'] = obj.get('url')
      self.entity.type = self.entity.published.get('type') or models.get_type(obj)
      self.entity.type_label = self.source.TYPE_LABELS.get(self.entity.type)
      self.response.headers['Content-Type'] = 'application/json'
      logging.info('Returning %s', json.dumps(self.entity.published, indent=2))
      self.response.headers['Location'] = self.entity.published['url'].encode('utf-8')
      self.response.status = 201
      return gr_source.creation_result(
        json.dumps(self.entity.published, indent=2))
Ejemplo n.º 18
0
  def attempt_single_item(self, item):
    """Attempts to preview or publish a single mf2 item.

    Args:
      item: mf2 item dict from mf2py

    Returns:
      CreationResult
    """
    self.maybe_inject_silo_content(item)
    obj = microformats2.json_to_object(item)

    ignore_formatting = self.ignore_formatting(item)
    if ignore_formatting:
      prop = microformats2.first_props(item.get('properties', {}))
      content = microformats2.get_text(prop.get('content'))
      if content:
        obj['content'] = content.strip()

    # which original post URL to include? in order of preference:
    # 1. rel-shortlink (background: https://github.com/snarfed/bridgy/issues/173)
    # 2. original user-provided URL if it redirected
    # 3. u-url if available
    # 4. actual final fetched URL
    if self.shortlink:
      obj['url'] = self.shortlink
    elif self.source_url() != self.fetched.url:
      obj['url'] = self.source_url()
    elif 'url' not in obj:
      obj['url'] = self.fetched.url
    logging.debug('Converted to ActivityStreams object: %s', json.dumps(obj, indent=2))

    # posts and comments need content
    obj_type = obj.get('objectType')
    if obj_type in ('note', 'article', 'comment'):
      if (not obj.get('content') and not obj.get('summary') and
          not obj.get('displayName')):
        return gr_source.creation_result(
          abort=False,
          error_plain='Could not find content in %s' % self.fetched.url,
          error_html='Could not find <a href="http://microformats.org/">content</a> in %s' % self.fetched.url)

    self.preprocess(obj)

    include_link = self.include_link(item)

    if not self.authorize():
      return gr_source.creation_result(abort=True)

    # RIP Facebook comments/likes. https://github.com/snarfed/bridgy/issues/350
    if (isinstance(self.source, FacebookPage) and
        (obj_type == 'comment' or obj.get('verb') == 'like')):
      return gr_source.creation_result(
        abort=True,
        error_plain='Facebook comments and likes are no longer supported. :(',
        error_html='<a href="https://github.com/snarfed/bridgy/issues/350">'
                   'Facebook comments and likes are no longer supported.</a> :(')

    if self.PREVIEW:
      result = self.source.gr_source.preview_create(
        obj, include_link=include_link, ignore_formatting=ignore_formatting)
      self.entity.published = result.content or result.description
      if not self.entity.published:
        return result  # there was an error
      state = {
        'source_key': self.source.key.urlsafe(),
        'source_url': self.source_url(),
        'target_url': self.target_url(),
        'include_link': include_link,
      }
      vars = {'source': self.preprocess_source(self.source),
              'preview': result.content,
              'description': result.description,
              'webmention_endpoint': self.request.host_url + '/publish/webmention',
              'state': util.encode_oauth_state(state),
              }
      vars.update(state)
      logging.info('Rendering preview with template vars %s', pprint.pformat(vars))
      return gr_source.creation_result(
        JINJA_ENV.get_template('preview.html').render(**vars))

    else:
      result = self.source.gr_source.create(
        obj, include_link=include_link, ignore_formatting=ignore_formatting)
      self.entity.published = result.content
      if not result.content:
        return result  # there was an error
      if 'url' not in self.entity.published:
        self.entity.published['url'] = obj.get('url')
      self.entity.type = self.entity.published.get('type') or models.get_type(obj)
      self.entity.type_label = self.source.TYPE_LABELS.get(self.entity.type)
      self.response.headers['Content-Type'] = 'application/json'
      logging.info('Returning %s', json.dumps(self.entity.published, indent=2))
      self.response.headers['Location'] = self.entity.published['url'].encode('utf-8')
      self.response.status = 201
      return gr_source.creation_result(
        json.dumps(self.entity.published, indent=2))