def path_join(base, path): """Joins 'base' and 'path' ('path' is interpreted as a relative path). This method is like os.path.join(), but 'path' is interpreted relatively. E.g., os.path.join('/a/b', '/c') yields '/c', but this function yields '/a/b/c'. Args: base: The base path. path: The path to append to base; this is treated as a relative path. Returns: The path obtaining by appending 'path' to 'base'. """ if os.path.isabs(path): # Remove drive letter (if we are on Windows). unused_drive, path_no_drive = os.path.splitdrive(path) # Remove leading path separator. path = path_no_drive[1:] return AbstractFileSystem.normpath(os.path.join(base, path))
def get_all_courses(rules_text=None): """Reads all course rewrite rule definitions from environment variable.""" # Normalize text definition. if not rules_text: rules_text = GCB_COURSES_CONFIG.value rules_text = rules_text.replace(',', '\n') # Use cached value if exists. cached = ApplicationContext.ALL_COURSE_CONTEXTS_CACHE.get(rules_text) if cached: return cached # Compute the list of contexts. rules = rules_text.split('\n') slugs = {} namespaces = {} all_contexts = [] for rule in rules: rule = rule.strip() if not rule or rule.startswith('#'): continue parts = rule.split(':') # validate length if len(parts) < 3: raise Exception('Expected rule definition of the form ' ' \'type:slug:folder[:ns]\', got %s: ' % rule) # validate type if parts[0] != SITE_TYPE_COURSE: raise Exception('Expected \'%s\', found: \'%s\'.' % (SITE_TYPE_COURSE, parts[0])) site_type = parts[0] # validate slug slug = parts[1] slug_parts = urlparse.urlparse(slug) if slug != slug_parts[2]: raise Exception( 'Bad rule: \'%s\'. ' 'Course URL prefix \'%s\' must be a simple URL fragment.' % ( rule, slug)) if slug in slugs: raise Exception( 'Bad rule: \'%s\'. ' 'Course URL prefix \'%s\' is already defined.' % (rule, slug)) slugs[slug] = True # validate folder name if parts[2]: folder = parts[2] # pylint: disable-msg=g-long-lambda create_fs = lambda unused_ns: LocalReadOnlyFileSystem( logical_home_folder=folder) else: folder = '/' # pylint: disable-msg=g-long-lambda create_fs = lambda ns: DatastoreBackedFileSystem( ns=ns, logical_home_folder=appengine_config.BUNDLE_ROOT, inherits_from=LocalReadOnlyFileSystem(logical_home_folder='/'), inheritable_folders=GCB_INHERITABLE_FOLDER_NAMES) # validate or derive namespace namespace = appengine_config.DEFAULT_NAMESPACE_NAME if len(parts) == 4: namespace = parts[3] else: if folder and folder != '/': namespace = '%s%s' % (GCB_BASE_COURSE_NAMESPACE, folder.replace('/', '-')) try: namespace_manager.validate_namespace(namespace) except Exception as e: raise Exception( 'Error validating namespace "%s" in rule "%s"; %s.' % ( namespace, rule, e)) if namespace in namespaces: raise Exception( 'Bad rule \'%s\'. ' 'Namespace \'%s\' is already defined.' % (rule, namespace)) namespaces[namespace] = True all_contexts.append(ApplicationContext( site_type, slug, folder, namespace, AbstractFileSystem(create_fs(namespace)), raw=rule)) _validate_appcontext_list(all_contexts) # Cache result to avoid re-parsing over and over. ApplicationContext.ALL_COURSE_CONTEXTS_CACHE = {rules_text: all_contexts} return all_contexts
def test_url_to_handler_mapping_for_course_type(): """Tests mapping of a URL to a handler for course type.""" # setup rules setup_courses('course:/a/b:/c/d, course:/e/f:/g/h') # setup helper classes class FakeHandler0(object): def __init__(self): self.app_context = None class FakeHandler1(object): def __init__(self): self.app_context = None class FakeHandler2(zipserve.ZipHandler): def __init__(self): self.app_context = None class FakeHandler3(zipserve.ZipHandler): def __init__(self): self.app_context = None class FakeHandler4(zipserve.ZipHandler): def __init__(self): self.app_context = None # Setup handler. handler0 = FakeHandler0 handler1 = FakeHandler1 handler2 = FakeHandler2 urls = [('/', handler0), ('/foo', handler1), ('/bar', handler2)] ApplicationRequestHandler.bind(urls) # Test proper handler mappings. assert_handled('/a/b', FakeHandler0) assert_handled('/a/b/', FakeHandler0) assert_handled('/a/b/foo', FakeHandler1) assert_handled('/a/b/bar', FakeHandler2) # Test partial path match. assert_handled('/a/b/foo/bee', None) assert_handled('/a/b/bar/bee', FakeHandler2) # Test assets mapping. handler = assert_handled('/a/b/assets/img/foo.png', AssetHandler) assert AbstractFileSystem.normpath( handler.app_context.get_template_home()).endswith( AbstractFileSystem.normpath('/c/d/views')) # This is allowed as we don't go out of /assets/... handler = assert_handled( '/a/b/assets/foo/../models/models.py', AssetHandler) assert AbstractFileSystem.normpath(handler.filename).endswith( AbstractFileSystem.normpath('/c/d/assets/models/models.py')) # This is not allowed as we do go out of /assets/... assert_handled('/a/b/assets/foo/../../models/models.py', None) # Test negative cases assert_handled('/foo', None) assert_handled('/baz', None) # Site 'views' and 'data' are not accessible assert_handled('/a/b/view/base.html', None) assert_handled('/a/b/data/units.csv', None) # Default mapping reset_courses() handler3 = FakeHandler3 handler4 = FakeHandler4 urls = [ ('/', handler0), ('/foo', handler1), ('/bar', handler2), ('/zip', handler3), ('/zip/a/b', handler4)] ApplicationRequestHandler.bind(urls) # Positive cases assert_handled('/', FakeHandler0) assert_handled('/foo', FakeHandler1) assert_handled('/bar', FakeHandler2) handler = assert_handled('/assets/js/main.js', AssetHandler) assert AbstractFileSystem.normpath( handler.app_context.get_template_home()).endswith( AbstractFileSystem.normpath('/views')) # Partial URL matching cases test that the most specific match is found. assert_handled('/zip', FakeHandler3) assert_handled('/zip/a', FakeHandler3) assert_handled('/zip/a/b', FakeHandler4) assert_handled('/zip/a/b/c', FakeHandler4) # Negative cases assert_handled('/baz', None) assert_handled('/favicon.ico', None) assert_handled('/e/f/index.html', None) assert_handled('/foo/foo.css', None) # Clean up. ApplicationRequestHandler.bind([])