2006-09-07 13:21:56 +02:00
|
|
|
import CryptoBox
|
|
|
|
import WebInterfaceDataset
|
|
|
|
import re
|
2006-09-14 14:33:01 +02:00
|
|
|
import Plugins
|
2006-09-12 10:55:20 +02:00
|
|
|
from CryptoBoxExceptions import *
|
2006-10-09 18:44:35 +02:00
|
|
|
import cherrypy
|
|
|
|
|
2006-09-14 14:33:01 +02:00
|
|
|
|
|
|
|
class WebInterfacePlugins:
|
|
|
|
|
|
|
|
def __init__(self, log, plugins, handler_func):
|
2006-09-25 14:22:41 +02:00
|
|
|
for plugin in plugins.getPlugins():
|
|
|
|
if not plugin: continue
|
|
|
|
plname = plugin.getName()
|
2006-09-14 14:33:01 +02:00
|
|
|
log.info("Plugin '%s' loaded" % plname)
|
|
|
|
## this should be the "easiest" way to expose all plugins as URLs
|
2006-09-25 14:22:41 +02:00
|
|
|
setattr(self, plname, handler_func(plugin))
|
2006-09-14 14:33:01 +02:00
|
|
|
setattr(getattr(self, plname), "exposed", True)
|
2006-09-25 14:22:41 +02:00
|
|
|
# TODO: check, if this really works - for now the "stream_response" feature seems to be broken
|
|
|
|
#setattr(getattr(self, plname), "stream_respones", True)
|
2006-09-14 14:33:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
class WebInterfaceSites:
|
|
|
|
'''
|
|
|
|
url2func = {'index':'show_status','doc':'show_doc','logs':'show_log'}
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
import logging
|
|
|
|
self.cbox = CryptoBox.CryptoBoxProps()
|
|
|
|
self.log = logging.getLogger("CryptoBox")
|
2006-09-08 13:02:27 +02:00
|
|
|
self.prefs = self.cbox.prefs
|
2006-09-07 13:21:56 +02:00
|
|
|
self.__resetDataset()
|
2006-09-12 10:55:20 +02:00
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
|
|
|
|
def __resetDataset(self):
|
2006-09-18 12:16:05 +02:00
|
|
|
"""this method has to be called at the beginning of every "site" action
|
|
|
|
important: only at the beginning of an action (to not loose information)
|
|
|
|
important: for _every_ "site" action (cherrypy is stateful)
|
2006-09-25 14:22:41 +02:00
|
|
|
also take care for the plugins, as they also contain datasets
|
2006-09-18 12:16:05 +02:00
|
|
|
"""
|
2006-09-25 14:22:41 +02:00
|
|
|
self.pluginList = Plugins.PluginManager(self.cbox, self.prefs["Locations"]["PluginDir"])
|
|
|
|
self.plugins = WebInterfacePlugins(self.log, self.pluginList, self.return_plugin_action)
|
|
|
|
## publish the url "/system" as an alias for "/plugins"
|
|
|
|
self.plugins.index = self.system
|
|
|
|
self.dataset = WebInterfaceDataset.WebInterfaceDataset(self.cbox, self.prefs, self.pluginList.getPlugins())
|
2006-09-07 13:21:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
def __check_config(self):
|
|
|
|
#TODO: from now on a cryptobox is always configured
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def __check_init_running(self):
|
|
|
|
#TODO: implement this check (is mkfs still running?)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
## this is a function decorator to check authentication
|
|
|
|
## it has to be defined before any page definition requiring authentification
|
2006-10-11 17:51:28 +02:00
|
|
|
def __requestAuth(self=None):
|
2006-10-09 18:44:35 +02:00
|
|
|
def check_credentials(site):
|
|
|
|
def _inner_wrapper(self, *args, **kargs):
|
|
|
|
import base64
|
|
|
|
## define a "non-allowed" function
|
|
|
|
user, password = None, None
|
|
|
|
try:
|
|
|
|
resp = cherrypy.request.headers["Authorization"][6:] # ignore "Basic "
|
|
|
|
(user, password) = base64.b64decode(resp).split(":",1)
|
|
|
|
except KeyError:
|
|
|
|
## no "authorization" header was sent
|
|
|
|
pass
|
|
|
|
except TypeError:
|
|
|
|
## invalid base64 string
|
|
|
|
pass
|
|
|
|
except AttributeError:
|
|
|
|
## no cherrypy response header defined
|
|
|
|
pass
|
2006-10-11 17:51:28 +02:00
|
|
|
authDict = self.cbox.prefs.userDB["admins"]
|
2006-10-09 18:44:35 +02:00
|
|
|
if user in authDict.keys():
|
2006-10-11 17:51:28 +02:00
|
|
|
if self.cbox.prefs.userDB.getDigest(password) == authDict[user]:
|
2006-10-09 18:44:35 +02:00
|
|
|
## ok: return the choosen page
|
|
|
|
self.cbox.log.info("access granted for: %s" % user)
|
|
|
|
return site(self, *args, **kargs)
|
|
|
|
else:
|
|
|
|
self.cbox.log.info("wrong password supplied for: %s" % user)
|
|
|
|
else:
|
|
|
|
self.cbox.log.info("unknown user: %s" % str(user))
|
|
|
|
## wrong credentials: return "access denied"
|
|
|
|
cherrypy.response.headers["WWW-Authenticate"] = '''Basic realm="Test-Arena"'''
|
|
|
|
cherrypy.response.status = 401
|
|
|
|
return self.__render("access_denied")
|
|
|
|
return _inner_wrapper
|
|
|
|
return check_credentials
|
|
|
|
|
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
######################################################################
|
|
|
|
## put real sites down here and don't forget to expose them at the end
|
|
|
|
|
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
@cherrypy.expose
|
2006-09-07 13:21:56 +02:00
|
|
|
def status(self, weblang=""):
|
|
|
|
'''shows the current status of the box
|
|
|
|
'''
|
|
|
|
self.__resetDataset()
|
|
|
|
self.__setWebLang(weblang)
|
|
|
|
if not self.__check_config():
|
|
|
|
self.dataset["Data.Warning"] = "NotInitialized"
|
|
|
|
return self.__render("form_init")
|
|
|
|
elif self.__check_init_running():
|
|
|
|
self.dataset["Data.Warning"] = "InitNotFinished"
|
|
|
|
self.dataset["Data.Redirect.Action"] = "form_config"
|
|
|
|
self.dataset["Data.Redirect.Delay"] = "30"
|
|
|
|
return self.__render("empty")
|
|
|
|
else:
|
|
|
|
self.dataset["Data.Redirect.Delay"] = "60"
|
|
|
|
return self.__render("show_status")
|
|
|
|
|
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
@cherrypy.expose
|
2006-09-07 13:21:56 +02:00
|
|
|
def doc(self,page="",weblang=""):
|
|
|
|
'''prints the offline wikipage
|
|
|
|
'''
|
|
|
|
import re
|
|
|
|
self.__resetDataset()
|
|
|
|
self.__setWebLang(weblang)
|
|
|
|
|
|
|
|
## check for invalid characters
|
|
|
|
if page and not re.search(u'\W', page):
|
|
|
|
self.dataset["Data.Doc.Page"] = page
|
|
|
|
else:
|
|
|
|
## display this page as default help page
|
|
|
|
self.dataset["Data.Doc.Page"] ="CryptoBoxUser"
|
|
|
|
|
|
|
|
return self.__render("show_doc")
|
2006-09-12 10:55:20 +02:00
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
@cherrypy.expose
|
2006-09-12 10:55:20 +02:00
|
|
|
def system(self, weblang=""):
|
2006-09-07 13:21:56 +02:00
|
|
|
self.__resetDataset()
|
|
|
|
self.__setWebLang(weblang)
|
|
|
|
return self.__render("form_system")
|
2006-10-09 18:44:35 +02:00
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
@cherrypy.expose
|
2006-09-07 13:21:56 +02:00
|
|
|
def index(self, weblang=""):
|
|
|
|
self.__resetDataset()
|
|
|
|
self.__setWebLang(weblang)
|
|
|
|
return self.__render("show_status")
|
|
|
|
|
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
@cherrypy.expose
|
2006-09-07 13:21:56 +02:00
|
|
|
def show_volume(self, device="", weblang=""):
|
|
|
|
self.__resetDataset()
|
|
|
|
self.__setWebLang(weblang)
|
|
|
|
if self.__setDevice(device):
|
|
|
|
return self.__render("show_volume")
|
|
|
|
else:
|
|
|
|
if self.cbox.getContainerList():
|
|
|
|
return self.__render("show_volumes")
|
|
|
|
else:
|
|
|
|
return self.__render("show_status")
|
|
|
|
|
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
@cherrypy.expose
|
2006-09-07 13:21:56 +02:00
|
|
|
def show_volumes(self, weblang=""):
|
|
|
|
self.__resetDataset()
|
|
|
|
self.__setWebLang(weblang)
|
|
|
|
return self.__render("show_volumes")
|
|
|
|
|
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
def return_plugin_action(self, plugin):
|
|
|
|
def handler(self, **args):
|
|
|
|
self.__resetDataset()
|
|
|
|
try:
|
|
|
|
self.__setWebLang(args["weblang"])
|
|
|
|
del args["weblang"]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
## check the device argument of volume plugins
|
|
|
|
if "volume" in plugin.pluginCapabilities:
|
2006-09-07 13:21:56 +02:00
|
|
|
try:
|
2006-10-09 18:44:35 +02:00
|
|
|
## initialize the dataset of the selected device if necessary
|
|
|
|
if self.__setDevice(args["device"]):
|
|
|
|
plugin.device = args["device"]
|
|
|
|
self.dataset.setCurrentDiskState(plugin.device)
|
2006-09-07 13:21:56 +02:00
|
|
|
else:
|
2006-10-09 18:44:35 +02:00
|
|
|
return self.__render("show_status")
|
|
|
|
except KeyError:
|
|
|
|
return self.__render("show_status")
|
2006-09-07 13:21:56 +02:00
|
|
|
else:
|
2006-10-09 18:44:35 +02:00
|
|
|
## the parameter 'device' exists - we have to remove it
|
|
|
|
del args["device"]
|
|
|
|
## call the plugin handler
|
|
|
|
nextTemplate = plugin.doAction(**args)
|
|
|
|
## for 'volume' plugins: reread the dataset of the current disk
|
|
|
|
## additionally: set the default template for plugins
|
|
|
|
if "volume" in plugin.pluginCapabilities:
|
|
|
|
self.dataset.setCurrentDiskState(plugin.device)
|
|
|
|
if not nextTemplate: nextTemplate = "show_volume"
|
2006-09-07 13:21:56 +02:00
|
|
|
else:
|
2006-10-11 17:51:28 +02:00
|
|
|
self.dataset.setPluginData()
|
2006-10-09 18:44:35 +02:00
|
|
|
if not nextTemplate: nextTemplate = "form_system"
|
|
|
|
## save the currently active plugin name
|
|
|
|
self.dataset["Data.ActivePlugin"] = plugin.getName()
|
|
|
|
return self.__render(nextTemplate, plugin)
|
|
|
|
## apply authentication?
|
2006-10-11 17:51:28 +02:00
|
|
|
if plugin.isAuthRequired():
|
|
|
|
return lambda **args: self.__requestAuth()(handler)(self, **args)
|
2006-09-07 13:21:56 +02:00
|
|
|
else:
|
2006-10-09 18:44:35 +02:00
|
|
|
return lambda **args: handler(self, **args)
|
2006-09-07 13:21:56 +02:00
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
|
|
|
|
## test authentication
|
|
|
|
@cherrypy.expose
|
2006-10-11 17:51:28 +02:00
|
|
|
@__requestAuth
|
2006-09-18 12:16:05 +02:00
|
|
|
def test(self, weblang=""):
|
2006-09-07 13:21:56 +02:00
|
|
|
self.__resetDataset()
|
2006-09-18 12:16:05 +02:00
|
|
|
self.__setWebLang(weblang)
|
2006-09-26 10:39:43 +02:00
|
|
|
return "test passed"
|
2006-10-09 18:44:35 +02:00
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
|
2006-10-09 18:44:35 +02:00
|
|
|
@cherrypy.expose
|
|
|
|
def test_stream(self):
|
|
|
|
"""just for testing purposes - to check if the "stream_response" feature
|
|
|
|
actually works - for now (September 02006) it does not seem to be ok"""
|
|
|
|
import time
|
|
|
|
yield "<html><head><title>neu</title></head><body><p><ul>"
|
|
|
|
for a in range(10):
|
|
|
|
yield "<li>yes: %d - %s</li>" % (a, str(time.time()))
|
|
|
|
time.sleep(1)
|
|
|
|
yield "</ul></p></html>"
|
2006-09-07 13:21:56 +02:00
|
|
|
|
2006-09-12 10:55:20 +02:00
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
|
|
|
|
##################### input checker ##########################
|
|
|
|
def __setWebLang(self, value):
|
|
|
|
guess = value
|
|
|
|
availLangs = self.cbox.getAvailableLanguages()
|
2006-09-26 10:39:43 +02:00
|
|
|
## no language specified: check browser language
|
2006-09-25 14:22:41 +02:00
|
|
|
if not guess:
|
|
|
|
guess = self.__getPreferredBrowserLanguage(availLangs)
|
2006-09-07 13:21:56 +02:00
|
|
|
if not guess or \
|
2006-09-25 14:22:41 +02:00
|
|
|
not guess in availLangs or \
|
|
|
|
re.search(u'\W', guess):
|
|
|
|
self.cbox.log.info("invalid language choosen: %s" % guess)
|
|
|
|
guess = self.prefs["WebSettings"]["Language"]
|
2006-09-07 13:21:56 +02:00
|
|
|
## maybe the language is still not valid
|
|
|
|
if not guess in availLangs:
|
|
|
|
self.log.warn("the configured language is invalid: %s" % guess)
|
2006-09-25 14:22:41 +02:00
|
|
|
guess = "en"
|
|
|
|
## maybe there is no english dataset???
|
|
|
|
if not guess in availLangs:
|
|
|
|
self.log.warn("couldn't find the english dataset")
|
2006-09-07 13:21:56 +02:00
|
|
|
guess = availLangs[0]
|
|
|
|
self.dataset["Settings.Language"] = guess
|
2006-09-25 14:22:41 +02:00
|
|
|
## we only have to save it, if it was specified correctly and explicitly
|
|
|
|
if value == guess:
|
|
|
|
self.dataset["Settings.LinkAttrs.weblang"] = guess
|
|
|
|
|
|
|
|
|
|
|
|
def __getPreferredBrowserLanguage(self, availLangs):
|
|
|
|
"""guess the preferred language of the user (as sent by the browser)
|
|
|
|
take the first language, that is part of 'availLangs'
|
|
|
|
"""
|
|
|
|
import cherrypy
|
|
|
|
try:
|
|
|
|
pref_lang_header = cherrypy.request.headers["Accept-Language"]
|
|
|
|
except KeyError:
|
|
|
|
## no language header was specified
|
|
|
|
return None
|
|
|
|
## this could be a typical 'Accept-Language' header:
|
|
|
|
## de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
|
|
|
|
regex = re.compile(u"\w+(-\w+)?(;q=[\d\.]+)?$")
|
|
|
|
pref_langs = [e.split(";",1)[0]
|
|
|
|
for e in pref_lang_header.split(",")
|
|
|
|
if regex.match(e)]
|
|
|
|
## is one of these preferred languages available?
|
|
|
|
for lang in pref_langs:
|
|
|
|
if lang in availLangs: return lang
|
|
|
|
## we try to be nice: also look for "de" if "de-de" was specified ...
|
|
|
|
for lang in pref_langs:
|
|
|
|
## use only the first part of the language
|
|
|
|
short_lang = lang.split("-",1)[0]
|
|
|
|
if short_lang in availLangs: return short_lang
|
|
|
|
## we give up
|
|
|
|
return None
|
2006-09-07 13:21:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
def __setDevice(self, device):
|
|
|
|
if device and re.match(u'[\w /\-]+$', device) and self.cbox.getContainer(device):
|
|
|
|
self.log.debug("select device: %s" % device)
|
|
|
|
self.dataset.setCurrentDiskState(device)
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
self.log.warn("invalid device: %s" % device)
|
|
|
|
self.dataset["Data.Warning"] = "InvalidDevice"
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def __checkVolumeName(self, name):
|
|
|
|
if name and re.match(u'[\w \-]+$', name):
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2006-09-25 14:22:41 +02:00
|
|
|
|
2006-09-26 10:39:43 +02:00
|
|
|
def __getLanguageValue(self, value):
|
|
|
|
hdf = self.__getLanguageData(self.dataset["Settings.Language"])
|
|
|
|
return hdf.getValue(value, "")
|
|
|
|
|
|
|
|
|
2006-09-25 14:22:41 +02:00
|
|
|
def __getLanguageData(self, web_lang="en"):
|
|
|
|
import neo_cgi, neo_util, os
|
|
|
|
default_lang = "en"
|
|
|
|
conf_lang = self.prefs["WebSettings"]["Language"]
|
|
|
|
hdf = neo_util.HDF()
|
|
|
|
langDir = os.path.abspath(self.prefs["Locations"]["LangDir"])
|
|
|
|
langFiles = []
|
|
|
|
## first: read default language (en)
|
|
|
|
if (default_lang != conf_lang) and (default_lang != web_lang):
|
|
|
|
langFiles.append(os.path.join(langDir, default_lang + ".hdf"))
|
|
|
|
## second: read language as defined in the config file
|
|
|
|
if (conf_lang != web_lang):
|
|
|
|
langFiles.append(os.path.join(langDir, conf_lang + ".hdf"))
|
|
|
|
## third: read language as configured via web interface
|
|
|
|
langFiles.append(os.path.join(langDir, web_lang + ".hdf"))
|
|
|
|
for langFile in langFiles:
|
|
|
|
if os.access(langFile, os.R_OK):
|
|
|
|
hdf.readFile(langFile)
|
|
|
|
else:
|
|
|
|
log.warn("Couldn't read language file: %s" % langFile)
|
|
|
|
return hdf
|
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
|
2006-09-25 14:22:41 +02:00
|
|
|
def __render(self, renderInfo, plugin=None):
|
2006-09-07 13:21:56 +02:00
|
|
|
'''renders from clearsilver templates and returns the resulting html
|
|
|
|
'''
|
2006-09-25 14:22:41 +02:00
|
|
|
import os, types
|
2006-09-07 13:21:56 +02:00
|
|
|
try:
|
|
|
|
import neo_cgi, neo_util, neo_cs
|
|
|
|
except ImportError:
|
2006-09-14 14:33:01 +02:00
|
|
|
errorMsg = "Could not import clearsilver module. Try 'apt-get install python-clearsilver'."
|
2006-09-07 13:21:56 +02:00
|
|
|
self.log.error(errorMsg)
|
|
|
|
sys.stderr.write(errorMsg)
|
|
|
|
raise ImportError, errorMsg
|
|
|
|
|
2006-09-25 14:22:41 +02:00
|
|
|
## is renderInfo a string (filename of the template) or a dictionary?
|
|
|
|
if type(renderInfo) == types.DictType:
|
|
|
|
template = renderInfo["template"]
|
|
|
|
if renderInfo.has_key("generator"):
|
|
|
|
generator = renderInfo["generator"]
|
|
|
|
else:
|
|
|
|
generator = False
|
|
|
|
else:
|
|
|
|
(template, generator) = (renderInfo, None)
|
|
|
|
|
|
|
|
## load the language data
|
|
|
|
hdf = neo_util.HDF()
|
|
|
|
hdf.copy("Lang", self.__getLanguageData(self.dataset["Settings.Language"]))
|
|
|
|
|
|
|
|
## first: assume, that the template file is in the global template directory
|
|
|
|
self.dataset["Settings.TemplateFile"] = os.path.abspath(os.path.join(self.prefs["Locations"]["TemplateDir"], template + ".cs"))
|
|
|
|
|
2006-09-14 14:33:01 +02:00
|
|
|
if plugin:
|
2006-09-25 14:22:41 +02:00
|
|
|
## check, if the plugin provides the template file -> overriding
|
|
|
|
plugin_cs_file = plugin.getTemplateFileName(template)
|
|
|
|
if plugin_cs_file:
|
|
|
|
self.dataset["Settings.TemplateFile"] = plugin_cs_file
|
2006-10-09 18:44:35 +02:00
|
|
|
|
2006-09-25 14:22:41 +02:00
|
|
|
## add the current state of the plugins to the hdf dataset
|
|
|
|
self.dataset["Data.Status.Plugins.%s" % plugin.getName()] = plugin.getStatus()
|
|
|
|
## load the language data
|
|
|
|
pl_lang = plugin.getLanguageData(self.dataset["Settings.Language"])
|
|
|
|
if pl_lang:
|
|
|
|
hdf.copy("Lang.Plugins.%s" % plugin.getName(), pl_lang)
|
|
|
|
## load the dataset of the plugin
|
|
|
|
plugin.loadDataSet(hdf)
|
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
self.log.info("rendering site: " + template)
|
|
|
|
|
2006-09-25 14:22:41 +02:00
|
|
|
cs_path = os.path.abspath(os.path.join(self.prefs["Locations"]["TemplateDir"], "main.cs"))
|
2006-09-07 13:21:56 +02:00
|
|
|
if not os.access(cs_path, os.R_OK):
|
|
|
|
log.error("Couldn't read clearsilver file: %s" % cs_path)
|
2006-09-25 14:22:41 +02:00
|
|
|
yield "Couldn't read clearsilver file: %s" % cs_path
|
|
|
|
return
|
2006-09-12 10:55:20 +02:00
|
|
|
|
2006-09-25 14:22:41 +02:00
|
|
|
## update the container hdf-dataset (necessary if a plugin changed the state of a container)
|
2006-09-18 12:16:05 +02:00
|
|
|
self.dataset.setContainersState()
|
|
|
|
|
2006-09-07 13:21:56 +02:00
|
|
|
self.log.debug(self.dataset)
|
|
|
|
for key in self.dataset.keys():
|
|
|
|
hdf.setValue(key,str(self.dataset[key]))
|
|
|
|
cs = neo_cs.CS(hdf)
|
|
|
|
cs.parseFile(cs_path)
|
2006-09-25 14:22:41 +02:00
|
|
|
|
|
|
|
## is there a generator containing additional information?
|
|
|
|
if generator is None:
|
|
|
|
## all content in one flush
|
|
|
|
yield cs.render()
|
|
|
|
else:
|
|
|
|
content_generate = generator()
|
|
|
|
dummy_line = """<!-- CONTENT_DUMMY -->"""
|
|
|
|
## now we do it linewise - checking for the content marker
|
|
|
|
for line in cs.render().splitlines():
|
|
|
|
if line.find(dummy_line) != -1:
|
|
|
|
yield line.replace(dummy_line, content_generate.next())
|
|
|
|
else:
|
|
|
|
yield line + "\n"
|
|
|
|
|
|
|
|
|