First alpha release (#52)
* added __version__ to main.py * package name changed from io.lbry.lbrynet to io.lbry.browser * removed unnecessary WRITE_EXTERNAL_STORAGE permission from AndroidManifest template, buildozer.spec updates and some cleanup
This commit is contained in:
parent
c9d8fa1e85
commit
76dee67e9a
298 changed files with 43 additions and 40521 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@ build.log
|
||||||
recipes/**/*.pyc
|
recipes/**/*.pyc
|
||||||
src/main/assets/index.android.bundle
|
src/main/assets/index.android.bundle
|
||||||
src/main/assets/index.android.bundle.meta
|
src/main/assets/index.android.bundle.meta
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,7 @@ class MediaPlayer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.clearControlsTimeout();
|
||||||
this.setState({ paused: true, fullscreenMode: false });
|
this.setState({ paused: true, fullscreenMode: false });
|
||||||
const { onFullscreenToggled } = this.props;
|
const { onFullscreenToggled } = this.props;
|
||||||
if (onFullscreenToggled) {
|
if (onFullscreenToggled) {
|
||||||
|
|
|
@ -58,7 +58,7 @@ const filePageStyle = StyleSheet.create({
|
||||||
},
|
},
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
width: screenWidth,
|
width: screenWidth,
|
||||||
height: 200
|
height: 204
|
||||||
},
|
},
|
||||||
downloadButton: {
|
downloadButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
[app]
|
[app]
|
||||||
|
|
||||||
# (str) Title of your application
|
# (str) Title of your application
|
||||||
title = lbrynet
|
title = LBRY Browser
|
||||||
|
|
||||||
# (str) Package name
|
# (str) Package name
|
||||||
package.name = lbrynet
|
package.name = browser
|
||||||
|
|
||||||
# (str) Package domain (needed for android/ios packaging)
|
# (str) Package domain (needed for android/ios packaging)
|
||||||
package.domain = io.lbry
|
package.domain = io.lbry
|
||||||
|
@ -28,11 +28,11 @@ source.include_exts = py,png,jpg,kv,atlas
|
||||||
#source.exclude_patterns = license,images/*/*.jpg
|
#source.exclude_patterns = license,images/*/*.jpg
|
||||||
|
|
||||||
# (str) Application versioning (method 1)
|
# (str) Application versioning (method 1)
|
||||||
version = 0.1
|
#version = 0.1
|
||||||
|
|
||||||
# (str) Application versioning (method 2)
|
# (str) Application versioning (method 2)
|
||||||
# version.regex = __version__ = ['"](.*)['"]
|
version.regex = __version__ = ['"](.*)['"]
|
||||||
# version.filename = %(source.dir)s/main.py
|
version.filename = %(source.dir)s/main.py
|
||||||
|
|
||||||
# (list) Application requirements
|
# (list) Application requirements
|
||||||
# comma seperated e.g. requirements = sqlite3,kivy
|
# comma seperated e.g. requirements = sqlite3,kivy
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
[app]
|
[app]
|
||||||
|
|
||||||
# (str) Title of your application
|
# (str) Title of your application
|
||||||
title = lbrynet
|
title = LBRY Browser
|
||||||
|
|
||||||
# (str) Package name
|
# (str) Package name
|
||||||
package.name = lbrynet
|
package.name = browser
|
||||||
|
|
||||||
# (str) Package domain (needed for android/ios packaging)
|
# (str) Package domain (needed for android/ios packaging)
|
||||||
package.domain = io.lbry
|
package.domain = io.lbry
|
||||||
|
@ -28,11 +28,11 @@ source.include_exts = py,png,jpg,kv,atlas
|
||||||
#source.exclude_patterns = license,images/*/*.jpg
|
#source.exclude_patterns = license,images/*/*.jpg
|
||||||
|
|
||||||
# (str) Application versioning (method 1)
|
# (str) Application versioning (method 1)
|
||||||
version = 0.1
|
#version = 0.1
|
||||||
|
|
||||||
# (str) Application versioning (method 2)
|
# (str) Application versioning (method 2)
|
||||||
# version.regex = __version__ = ['"](.*)['"]
|
version.regex = __version__ = ['"](.*)['"]
|
||||||
# version.filename = %(source.dir)s/main.py
|
version.filename = %(source.dir)s/main.py
|
||||||
|
|
||||||
# (list) Application requirements
|
# (list) Application requirements
|
||||||
# comma seperated e.g. requirements = sqlite3,kivy
|
# comma seperated e.g. requirements = sqlite3,kivy
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
from pythonforandroid.toolchain import Bootstrap
|
|
||||||
from os.path import join, exists
|
|
||||||
from os import walk
|
|
||||||
import glob
|
|
||||||
import sh
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyBootstrap(Bootstrap):
|
|
||||||
name = 'empty'
|
|
||||||
|
|
||||||
recipe_depends = []
|
|
||||||
|
|
||||||
can_be_chosen_automatically = False
|
|
||||||
|
|
||||||
def run_distribute(self):
|
|
||||||
print('empty bootstrap has no distribute')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
bootstrap = EmptyBootstrap()
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -255,7 +255,7 @@ main.py that loads it.''')
|
||||||
url_scheme = 'kivy'
|
url_scheme = 'kivy'
|
||||||
|
|
||||||
# Prepare some variables for templating process
|
# Prepare some variables for templating process
|
||||||
default_icon = 'templates/kivy-icon.png'
|
default_icon = 'templates/lbry-icon.png'
|
||||||
default_presplash = 'templates/kivy-presplash.jpg'
|
default_presplash = 'templates/kivy-presplash.jpg'
|
||||||
shutil.copy(args.icon or default_icon, 'src/main/res/drawable/icon.png')
|
shutil.copy(args.icon or default_icon, 'src/main/res/drawable/icon.png')
|
||||||
shutil.copy(args.presplash or default_presplash,
|
shutil.copy(args.presplash or default_presplash,
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
<!-- OpenGL ES 2.0 -->
|
<!-- OpenGL ES 2.0 -->
|
||||||
<uses-feature android:glEsVersion="0x00020000" />
|
<uses-feature android:glEsVersion="0x00020000" />
|
||||||
|
|
||||||
<!-- Allow writing to external storage -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
{% for perm in args.permissions %}
|
{% for perm in args.permissions %}
|
||||||
{% if '.' in perm %}
|
{% if '.' in perm %}
|
||||||
<uses-permission android:name="{{ perm }}" />
|
<uses-permission android:name="{{ perm }}" />
|
||||||
|
@ -69,7 +67,7 @@
|
||||||
|
|
||||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||||
|
|
||||||
<activity android:name="io.lbry.lbrynet.MainActivity"
|
<activity android:name="io.lbry.browser.MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||||
android:configChanges="keyboardHidden|orientation{% if args.min_sdk_version >= 13 %}|screenSize{% endif %}"
|
android:configChanges="keyboardHidden|orientation{% if args.min_sdk_version >= 13 %}|screenSize{% endif %}"
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
tools:context="io.lbry.lbrynet.ServiceControlActivity"
|
tools:context="io.lbry.browser.ServiceControlActivity"
|
||||||
>
|
>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -1,3 +1,3 @@
|
||||||
rootProject.name = 'lbrynet'
|
rootProject.name = 'browser'
|
||||||
include ':react-native-video'
|
include ':react-native-video'
|
||||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-video/android')
|
project(':react-native-video').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-video/android')
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
from pythonforandroid.toolchain import Bootstrap, shprint, current_directory, info, warning, ArchARM, info_main
|
|
||||||
from os.path import join, exists
|
|
||||||
from os import walk
|
|
||||||
import glob
|
|
||||||
import sh
|
|
||||||
|
|
||||||
|
|
||||||
class PygameBootstrap(Bootstrap):
|
|
||||||
name = 'pygame'
|
|
||||||
|
|
||||||
recipe_depends = ['hostpython2', 'python2', 'pyjnius', 'sdl', 'pygame',
|
|
||||||
'android', 'kivy']
|
|
||||||
|
|
||||||
def run_distribute(self):
|
|
||||||
info_main('# Creating Android project from build and {} bootstrap'.format(
|
|
||||||
self.name))
|
|
||||||
|
|
||||||
# src_path = join(self.ctx.root_dir, 'bootstrap_templates',
|
|
||||||
# self.name)
|
|
||||||
src_path = join(self.bootstrap_dir, 'build')
|
|
||||||
|
|
||||||
arch = self.ctx.archs[0]
|
|
||||||
if len(self.ctx.archs) > 1:
|
|
||||||
raise ValueError('built for more than one arch, but bootstrap cannot handle that yet')
|
|
||||||
info('Bootstrap running with arch {}'.format(arch))
|
|
||||||
|
|
||||||
with current_directory(self.dist_dir):
|
|
||||||
|
|
||||||
info('Creating initial layout')
|
|
||||||
for dirname in ('assets', 'bin', 'private', 'res', 'templates'):
|
|
||||||
if not exists(dirname):
|
|
||||||
shprint(sh.mkdir, dirname)
|
|
||||||
|
|
||||||
info('Copying default files')
|
|
||||||
shprint(sh.cp, '-a', join(self.build_dir, 'project.properties'), '.')
|
|
||||||
shprint(sh.cp, '-a', join(src_path, 'build.py'), '.')
|
|
||||||
shprint(sh.cp, '-a', join(src_path, 'buildlib'), '.')
|
|
||||||
shprint(sh.cp, '-a', join(src_path, 'src'), '.')
|
|
||||||
shprint(sh.cp, '-a', join(src_path, 'templates'), '.')
|
|
||||||
shprint(sh.cp, '-a', join(src_path, 'res'), '.')
|
|
||||||
shprint(sh.cp, '-a', join(src_path, 'blacklist.txt'), '.')
|
|
||||||
shprint(sh.cp, '-a', join(src_path, 'whitelist.txt'), '.')
|
|
||||||
|
|
||||||
with open('local.properties', 'w') as fileh:
|
|
||||||
fileh.write('sdk.dir={}'.format(self.ctx.sdk_dir))
|
|
||||||
|
|
||||||
info('Copying python distribution')
|
|
||||||
hostpython = sh.Command(self.ctx.hostpython)
|
|
||||||
try:
|
|
||||||
shprint(hostpython, '-OO', '-m', 'compileall', self.ctx.get_python_install_dir(),
|
|
||||||
_tail=10, _filterout="^Listing")
|
|
||||||
except sh.ErrorReturnCode:
|
|
||||||
pass
|
|
||||||
if not exists('python-install'):
|
|
||||||
shprint(sh.cp, '-a', self.ctx.get_python_install_dir(), './python-install')
|
|
||||||
|
|
||||||
self.distribute_libs(arch, [join(self.build_dir, 'libs', arch.arch), self.ctx.get_libs_dir(arch.arch)]);
|
|
||||||
self.distribute_aars(arch)
|
|
||||||
self.distribute_javaclasses(self.ctx.javaclass_dir)
|
|
||||||
|
|
||||||
info('Filling private directory')
|
|
||||||
if not exists(join('private', 'lib')):
|
|
||||||
shprint(sh.cp, '-a', join('python-install', 'lib'), 'private')
|
|
||||||
shprint(sh.mkdir, '-p', join('private', 'include', 'python2.7'))
|
|
||||||
|
|
||||||
shprint(sh.mv, join('libs', arch.arch, 'libpymodules.so'), 'private/')
|
|
||||||
shprint(sh.cp, join('python-install', 'include' , 'python2.7', 'pyconfig.h'), join('private', 'include', 'python2.7/'))
|
|
||||||
|
|
||||||
info('Removing some unwanted files')
|
|
||||||
shprint(sh.rm, '-f', join('private', 'lib', 'libpython2.7.so'))
|
|
||||||
shprint(sh.rm, '-rf', join('private', 'lib', 'pkgconfig'))
|
|
||||||
|
|
||||||
with current_directory(join(self.dist_dir, 'private', 'lib', 'python2.7')):
|
|
||||||
# shprint(sh.xargs, 'rm', sh.grep('-E', '*\.(py|pyx|so\.o|so\.a|so\.libs)$', sh.find('.')))
|
|
||||||
removes = []
|
|
||||||
for dirname, something, filens in walk('.'):
|
|
||||||
for filename in filens:
|
|
||||||
for suffix in ('py', 'pyc', 'so.o', 'so.a', 'so.libs'):
|
|
||||||
if filename.endswith(suffix):
|
|
||||||
removes.append(filename)
|
|
||||||
shprint(sh.rm, '-f', *removes)
|
|
||||||
|
|
||||||
info('Deleting some other stuff not used on android')
|
|
||||||
# To quote the original distribute.sh, 'well...'
|
|
||||||
# shprint(sh.rm, '-rf', 'ctypes')
|
|
||||||
shprint(sh.rm, '-rf', 'lib2to3')
|
|
||||||
shprint(sh.rm, '-rf', 'idlelib')
|
|
||||||
for filename in glob.glob('config/libpython*.a'):
|
|
||||||
shprint(sh.rm, '-f', filename)
|
|
||||||
shprint(sh.rm, '-rf', 'config/python.o')
|
|
||||||
shprint(sh.rm, '-rf', 'lib-dynload/_ctypes_test.so')
|
|
||||||
shprint(sh.rm, '-rf', 'lib-dynload/_testcapi.so')
|
|
||||||
|
|
||||||
|
|
||||||
self.strip_libraries(arch)
|
|
||||||
super(PygameBootstrap, self).run_distribute()
|
|
||||||
|
|
||||||
bootstrap = PygameBootstrap()
|
|
|
@ -1,97 +0,0 @@
|
||||||
# eggs
|
|
||||||
*.egg-info
|
|
||||||
|
|
||||||
# unit test
|
|
||||||
unittest/*
|
|
||||||
|
|
||||||
# python config
|
|
||||||
config/makesetup
|
|
||||||
|
|
||||||
# unused pygame files
|
|
||||||
pygame/_camera_*
|
|
||||||
pygame/camera.pyo
|
|
||||||
pygame/*.html
|
|
||||||
pygame/*.bmp
|
|
||||||
pygame/*.svg
|
|
||||||
pygame/cdrom.so
|
|
||||||
pygame/pygame_icon.icns
|
|
||||||
pygame/LGPL
|
|
||||||
pygame/threads/Py25Queue.pyo
|
|
||||||
pygame/*.ttf
|
|
||||||
pygame/mac*
|
|
||||||
pygame/_numpy*
|
|
||||||
pygame/sndarray.pyo
|
|
||||||
pygame/surfarray.pyo
|
|
||||||
pygame/_arraysurfarray.pyo
|
|
||||||
|
|
||||||
# unused kivy files (platform specific)
|
|
||||||
kivy/input/providers/wm_*
|
|
||||||
kivy/input/providers/mactouch*
|
|
||||||
kivy/input/providers/probesysfs*
|
|
||||||
kivy/input/providers/mtdev*
|
|
||||||
kivy/input/providers/hidinput*
|
|
||||||
kivy/core/camera/camera_videocapture*
|
|
||||||
kivy/core/spelling/*osx*
|
|
||||||
kivy/core/video/video_pyglet*
|
|
||||||
|
|
||||||
# unused encodings
|
|
||||||
lib-dynload/*codec*
|
|
||||||
encodings/cp*.pyo
|
|
||||||
encodings/tis*
|
|
||||||
encodings/shift*
|
|
||||||
encodings/bz2*
|
|
||||||
encodings/iso*
|
|
||||||
encodings/undefined*
|
|
||||||
encodings/johab*
|
|
||||||
encodings/p*
|
|
||||||
encodings/m*
|
|
||||||
encodings/euc*
|
|
||||||
encodings/k*
|
|
||||||
encodings/unicode_internal*
|
|
||||||
encodings/quo*
|
|
||||||
encodings/gb*
|
|
||||||
encodings/big5*
|
|
||||||
encodings/hp*
|
|
||||||
encodings/hz*
|
|
||||||
|
|
||||||
# unused python modules
|
|
||||||
bsddb/*
|
|
||||||
wsgiref/*
|
|
||||||
hotshot/*
|
|
||||||
pydoc_data/*
|
|
||||||
tty.pyo
|
|
||||||
anydbm.pyo
|
|
||||||
nturl2path.pyo
|
|
||||||
LICENCE.txt
|
|
||||||
macurl2path.pyo
|
|
||||||
dummy_threading.pyo
|
|
||||||
audiodev.pyo
|
|
||||||
antigravity.pyo
|
|
||||||
dumbdbm.pyo
|
|
||||||
sndhdr.pyo
|
|
||||||
__phello__.foo.pyo
|
|
||||||
sunaudio.pyo
|
|
||||||
os2emxpath.pyo
|
|
||||||
multiprocessing/dummy*
|
|
||||||
|
|
||||||
# unused binaries python modules
|
|
||||||
lib-dynload/termios.so
|
|
||||||
lib-dynload/_lsprof.so
|
|
||||||
lib-dynload/*audioop.so
|
|
||||||
lib-dynload/mmap.so
|
|
||||||
lib-dynload/_hotshot.so
|
|
||||||
lib-dynload/_csv.so
|
|
||||||
lib-dynload/_heapq.so
|
|
||||||
lib-dynload/grp.so
|
|
||||||
lib-dynload/resource.so
|
|
||||||
lib-dynload/pyexpat.so
|
|
||||||
|
|
||||||
# odd files
|
|
||||||
plat-linux3/regen
|
|
||||||
|
|
||||||
#>sqlite3
|
|
||||||
# conditionnal include depending if some recipes are included or not.
|
|
||||||
sqlite3/*
|
|
||||||
lib-dynload/_sqlite3.so
|
|
||||||
#<sqlite3
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
# This file is used to override default values used by the Ant build system.
|
|
||||||
#
|
|
||||||
# This file must be checked in Version Control Systems, as it is
|
|
||||||
# integral to the build system of your project.
|
|
||||||
|
|
||||||
# This file is only used by the Ant script.
|
|
||||||
|
|
||||||
# You can use this to override default values such as
|
|
||||||
# 'source.dir' for the location of your java source folder and
|
|
||||||
# 'out.dir' for the location of your output folder.
|
|
||||||
|
|
||||||
# You can also use it define how the release builds are signed by declaring
|
|
||||||
# the following properties:
|
|
||||||
# 'key.store' for the location of your keystore and
|
|
||||||
# 'key.alias' for the name of the key to use.
|
|
||||||
# The password will be asked during the build when you use the 'release' target.
|
|
||||||
|
|
||||||
key.store=${env.P4A_RELEASE_KEYSTORE}
|
|
||||||
key.alias=${env.P4A_RELEASE_KEYALIAS}
|
|
||||||
key.store.password=${env.P4A_RELEASE_KEYSTORE_PASSWD}
|
|
||||||
key.alias.password=${env.P4A_RELEASE_KEYALIAS_PASSWD}
|
|
|
@ -1,533 +0,0 @@
|
||||||
#!/usr/bin/env python2.7
|
|
||||||
|
|
||||||
from os.path import dirname, join, isfile, realpath, relpath, split, exists
|
|
||||||
from zipfile import ZipFile
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, 'buildlib/jinja2.egg')
|
|
||||||
sys.path.insert(0, 'buildlib')
|
|
||||||
|
|
||||||
from fnmatch import fnmatch
|
|
||||||
import tarfile
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import jinja2
|
|
||||||
|
|
||||||
# The extension of the android and ant commands.
|
|
||||||
if os.name == 'nt':
|
|
||||||
ANDROID = 'android.bat'
|
|
||||||
ANT = 'ant.bat'
|
|
||||||
else:
|
|
||||||
ANDROID = 'android'
|
|
||||||
ANT = 'ant'
|
|
||||||
|
|
||||||
# if ANDROIDSDK is on path, use android from this path
|
|
||||||
ANDROIDSDK = os.environ.get('ANDROIDSDK')
|
|
||||||
if ANDROIDSDK:
|
|
||||||
ANDROID = os.path.join(ANDROIDSDK, 'tools', ANDROID)
|
|
||||||
|
|
||||||
curdir = dirname(__file__)
|
|
||||||
|
|
||||||
# Try to find a host version of Python that matches our ARM version.
|
|
||||||
PYTHON = join(curdir, 'python-install', 'bin', 'python.host')
|
|
||||||
|
|
||||||
BLACKLIST_PATTERNS = [
|
|
||||||
# code versionning
|
|
||||||
'^*.hg/*',
|
|
||||||
'^*.git/*',
|
|
||||||
'^*.bzr/*',
|
|
||||||
'^*.svn/*',
|
|
||||||
|
|
||||||
# pyc/py
|
|
||||||
'*.pyc',
|
|
||||||
'*.py',
|
|
||||||
|
|
||||||
# temp files
|
|
||||||
'~',
|
|
||||||
'*.bak',
|
|
||||||
'*.swp',
|
|
||||||
]
|
|
||||||
|
|
||||||
WHITELIST_PATTERNS = []
|
|
||||||
|
|
||||||
python_files = []
|
|
||||||
|
|
||||||
|
|
||||||
# Used by render.
|
|
||||||
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
|
||||||
join(curdir, 'templates')))
|
|
||||||
|
|
||||||
|
|
||||||
def render(template, dest, **kwargs):
|
|
||||||
'''Using jinja2, render `template` to the filename `dest`, supplying the
|
|
||||||
|
|
||||||
keyword arguments as template parameters.
|
|
||||||
'''
|
|
||||||
|
|
||||||
template = environment.get_template(template)
|
|
||||||
text = template.render(**kwargs)
|
|
||||||
|
|
||||||
f = open(dest, 'wb')
|
|
||||||
f.write(text.encode('utf-8'))
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
def compile_dir(dfn):
|
|
||||||
'''
|
|
||||||
Compile *.py in directory `dfn` to *.pyo
|
|
||||||
'''
|
|
||||||
|
|
||||||
# -OO = strip docstrings
|
|
||||||
subprocess.call([PYTHON, '-OO', '-m', 'compileall', '-f', dfn])
|
|
||||||
|
|
||||||
|
|
||||||
def is_whitelist(name):
|
|
||||||
return match_filename(WHITELIST_PATTERNS, name)
|
|
||||||
|
|
||||||
|
|
||||||
def is_blacklist(name):
|
|
||||||
if is_whitelist(name):
|
|
||||||
return False
|
|
||||||
return match_filename(BLACKLIST_PATTERNS, name)
|
|
||||||
|
|
||||||
|
|
||||||
def match_filename(pattern_list, name):
|
|
||||||
for pattern in pattern_list:
|
|
||||||
if pattern.startswith('^'):
|
|
||||||
pattern = pattern[1:]
|
|
||||||
else:
|
|
||||||
pattern = '*/' + pattern
|
|
||||||
if fnmatch(name, pattern):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def listfiles(d):
|
|
||||||
basedir = d
|
|
||||||
subdirlist = []
|
|
||||||
for item in os.listdir(d):
|
|
||||||
fn = join(d, item)
|
|
||||||
if isfile(fn):
|
|
||||||
yield fn
|
|
||||||
else:
|
|
||||||
subdirlist.append(os.path.join(basedir, item))
|
|
||||||
for subdir in subdirlist:
|
|
||||||
for fn in listfiles(subdir):
|
|
||||||
yield fn
|
|
||||||
|
|
||||||
|
|
||||||
def make_pythonzip():
|
|
||||||
'''
|
|
||||||
Search for all the python related files, and construct the pythonXX.zip
|
|
||||||
According to
|
|
||||||
# http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html
|
|
||||||
site-packages, config and lib-dynload will be not included.
|
|
||||||
'''
|
|
||||||
global python_files
|
|
||||||
d = realpath(join('private', 'lib', 'python2.7'))
|
|
||||||
|
|
||||||
# selector function
|
|
||||||
def select(fn):
|
|
||||||
if is_blacklist(fn):
|
|
||||||
return False
|
|
||||||
fn = realpath(fn)
|
|
||||||
assert(fn.startswith(d))
|
|
||||||
fn = fn[len(d):]
|
|
||||||
if (fn.startswith('/site-packages/') or
|
|
||||||
fn.startswith('/config/') or
|
|
||||||
fn.startswith('/lib-dynload/') or
|
|
||||||
fn.startswith('/libpymodules.so')):
|
|
||||||
return False
|
|
||||||
return fn
|
|
||||||
|
|
||||||
# get a list of all python file
|
|
||||||
python_files = [x for x in listfiles(d) if select(x)]
|
|
||||||
|
|
||||||
# create the final zipfile
|
|
||||||
zfn = join('private', 'lib', 'python27.zip')
|
|
||||||
zf = ZipFile(zfn, 'w')
|
|
||||||
|
|
||||||
# put all the python files in it
|
|
||||||
for fn in python_files:
|
|
||||||
afn = fn[len(d):]
|
|
||||||
zf.write(fn, afn)
|
|
||||||
zf.close()
|
|
||||||
|
|
||||||
|
|
||||||
def make_tar(tfn, source_dirs, ignore_path=[]):
|
|
||||||
'''
|
|
||||||
Make a zip file `fn` from the contents of source_dis.
|
|
||||||
'''
|
|
||||||
|
|
||||||
# selector function
|
|
||||||
def select(fn):
|
|
||||||
rfn = realpath(fn)
|
|
||||||
for p in ignore_path:
|
|
||||||
if p.endswith('/'):
|
|
||||||
p = p[:-1]
|
|
||||||
if rfn.startswith(p):
|
|
||||||
return False
|
|
||||||
if rfn in python_files:
|
|
||||||
return False
|
|
||||||
return not is_blacklist(fn)
|
|
||||||
|
|
||||||
# get the files and relpath file of all the directory we asked for
|
|
||||||
files = []
|
|
||||||
for sd in source_dirs:
|
|
||||||
sd = realpath(sd)
|
|
||||||
compile_dir(sd)
|
|
||||||
files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd)
|
|
||||||
if select(x)]
|
|
||||||
|
|
||||||
# create tar.gz of thoses files
|
|
||||||
tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT)
|
|
||||||
dirs = []
|
|
||||||
for fn, afn in files:
|
|
||||||
# print('%s: %s' % (tfn, fn))
|
|
||||||
dn = dirname(afn)
|
|
||||||
if dn not in dirs:
|
|
||||||
# create every dirs first if not exist yet
|
|
||||||
d = ''
|
|
||||||
for component in split(dn):
|
|
||||||
d = join(d, component)
|
|
||||||
if d.startswith('/'):
|
|
||||||
d = d[1:]
|
|
||||||
if d == '' or d in dirs:
|
|
||||||
continue
|
|
||||||
dirs.append(d)
|
|
||||||
tinfo = tarfile.TarInfo(d)
|
|
||||||
tinfo.type = tarfile.DIRTYPE
|
|
||||||
tf.addfile(tinfo)
|
|
||||||
|
|
||||||
# put the file
|
|
||||||
tf.add(fn, afn)
|
|
||||||
tf.close()
|
|
||||||
|
|
||||||
|
|
||||||
def make_package(args):
|
|
||||||
version_code = 0
|
|
||||||
manifest_extra = ['<uses-feature android:glEsVersion="0x00020000" />']
|
|
||||||
for filename in args.manifest_extra:
|
|
||||||
with open(filename, "r") as fd:
|
|
||||||
content = fd.read()
|
|
||||||
manifest_extra.append(content)
|
|
||||||
manifest_extra = '\n'.join(manifest_extra)
|
|
||||||
url_scheme = 'kivy'
|
|
||||||
default_icon = 'templates/kivy-icon.png'
|
|
||||||
default_presplash = 'templates/kivy-presplash.jpg'
|
|
||||||
default_ouya_icon = 'templates/kivy-ouya-icon.png'
|
|
||||||
# Figure out the version code, if necessary.
|
|
||||||
if not args.numeric_version:
|
|
||||||
for i in args.version.split('.'):
|
|
||||||
version_code *= 100
|
|
||||||
version_code += int(i)
|
|
||||||
|
|
||||||
args.numeric_version = str(version_code)
|
|
||||||
|
|
||||||
# args.name = args.name.decode('utf-8')
|
|
||||||
# if args.icon_name:
|
|
||||||
# args.icon_name = args.icon_name.decode('utf-8')
|
|
||||||
|
|
||||||
versioned_name = (args.name.replace(' ', '').replace('\'', '') +
|
|
||||||
'-' + args.version)
|
|
||||||
|
|
||||||
# Android SDK rev14 needs two ant execs (ex: debug installd) and
|
|
||||||
# new build.xml
|
|
||||||
build_tpl = 'build.xml'
|
|
||||||
|
|
||||||
if not args.icon_name:
|
|
||||||
args.icon_name = args.name
|
|
||||||
|
|
||||||
# Annoying fixups.
|
|
||||||
args.name = args.name.replace('\'', '\\\'')
|
|
||||||
args.icon_name = args.icon_name.replace('\'', '\\\'')
|
|
||||||
|
|
||||||
# Figure out versions of the private and public data.
|
|
||||||
private_version = str(time.time())
|
|
||||||
|
|
||||||
if args.dir:
|
|
||||||
public_version = private_version
|
|
||||||
else:
|
|
||||||
public_version = None
|
|
||||||
|
|
||||||
if args.intent_filters:
|
|
||||||
intent_filters = open(args.intent_filters).read()
|
|
||||||
else:
|
|
||||||
intent_filters = ''
|
|
||||||
|
|
||||||
directory = args.dir if public_version else args.private
|
|
||||||
# Ignore warning if the launcher is in args
|
|
||||||
if not args.launcher:
|
|
||||||
if not (exists(join(realpath(directory), 'main.py')) or
|
|
||||||
exists(join(realpath(directory), 'main.pyo'))):
|
|
||||||
print('''BUILD FAILURE: No main.py(o) found in your app directory.
|
|
||||||
This file must exist to act as the entry point for you app. If your app is
|
|
||||||
started by a file with a different name, rename it to main.py or add a
|
|
||||||
main.py that loads it.''')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Figure out if application has service part
|
|
||||||
service = False
|
|
||||||
if directory:
|
|
||||||
service_main = join(realpath(directory), 'service', 'main.py')
|
|
||||||
if os.path.exists(service_main) or os.path.exists(service_main + 'o'):
|
|
||||||
service = True
|
|
||||||
|
|
||||||
# Check if OUYA support is enabled
|
|
||||||
if args.ouya_category:
|
|
||||||
args.ouya_category = args.ouya_category.upper()
|
|
||||||
if args.ouya_category not in ('GAME', 'APP'):
|
|
||||||
print('Invalid --ouya-category argument. should be one of'
|
|
||||||
'GAME or APP')
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
# Render the various templates into control files.
|
|
||||||
render(
|
|
||||||
'AndroidManifest.tmpl.xml',
|
|
||||||
'AndroidManifest.xml',
|
|
||||||
args=args,
|
|
||||||
service=service,
|
|
||||||
url_scheme=url_scheme,
|
|
||||||
intent_filters=intent_filters,
|
|
||||||
manifest_extra=manifest_extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
render(
|
|
||||||
'Configuration.tmpl.java',
|
|
||||||
'src/org/renpy/android/Configuration.java',
|
|
||||||
args=args)
|
|
||||||
|
|
||||||
render(
|
|
||||||
build_tpl,
|
|
||||||
'build.xml',
|
|
||||||
args=args,
|
|
||||||
versioned_name=versioned_name)
|
|
||||||
|
|
||||||
render(
|
|
||||||
'strings.xml',
|
|
||||||
'res/values/strings.xml',
|
|
||||||
public_version=public_version,
|
|
||||||
private_version=private_version,
|
|
||||||
url_scheme=url_scheme,
|
|
||||||
args=args)
|
|
||||||
|
|
||||||
# Update the project to a recent version.
|
|
||||||
try:
|
|
||||||
subprocess.call([ANDROID, 'update', 'project', '-p', '.', '-t',
|
|
||||||
'android-{}'.format(args.sdk_version)])
|
|
||||||
except (OSError, IOError):
|
|
||||||
print('An error occured while calling', ANDROID, 'update')
|
|
||||||
print('Your PATH must include android tools.')
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
# Delete the old assets.
|
|
||||||
if os.path.exists('assets/public.mp3'):
|
|
||||||
os.unlink('assets/public.mp3')
|
|
||||||
|
|
||||||
if os.path.exists('assets/private.mp3'):
|
|
||||||
os.unlink('assets/private.mp3')
|
|
||||||
|
|
||||||
# In order to speedup import and initial depack,
|
|
||||||
# construct a python27.zip
|
|
||||||
make_pythonzip()
|
|
||||||
|
|
||||||
# Package up the private and public data.
|
|
||||||
if args.private:
|
|
||||||
make_tar('assets/private.mp3', ['private', args.private], args.ignore_path)
|
|
||||||
else:
|
|
||||||
make_tar('assets/private.mp3', ['private'])
|
|
||||||
|
|
||||||
if args.dir:
|
|
||||||
make_tar('assets/public.mp3', [args.dir], args.ignore_path)
|
|
||||||
|
|
||||||
# Copy over the icon and presplash files.
|
|
||||||
shutil.copy(args.icon or default_icon, 'res/drawable/icon.png')
|
|
||||||
shutil.copy(args.presplash or default_presplash,
|
|
||||||
'res/drawable/presplash.jpg')
|
|
||||||
|
|
||||||
# If OUYA support was requested, copy over the OUYA icon
|
|
||||||
if args.ouya_category:
|
|
||||||
if not os.path.isdir('res/drawable-xhdpi'):
|
|
||||||
os.mkdir('res/drawable-xhdpi')
|
|
||||||
shutil.copy(args.ouya_icon or default_ouya_icon,
|
|
||||||
'res/drawable-xhdpi/ouya_icon.png')
|
|
||||||
|
|
||||||
# If extra Java jars were requested, copy them into the libs directory
|
|
||||||
if args.add_jar:
|
|
||||||
for jarname in args.add_jar:
|
|
||||||
if not os.path.exists(jarname):
|
|
||||||
print('Requested jar does not exist: {}'.format(jarname))
|
|
||||||
sys.exit(-1)
|
|
||||||
shutil.copy(jarname, 'libs')
|
|
||||||
|
|
||||||
# Build.
|
|
||||||
try:
|
|
||||||
for arg in args.command:
|
|
||||||
subprocess.check_call([ANT, arg])
|
|
||||||
except (OSError, IOError):
|
|
||||||
print('An error occured while calling', ANT)
|
|
||||||
print('Did you install ant on your system ?')
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
def parse_args(args=None):
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
# get default SDK version from environment
|
|
||||||
android_api = os.environ.get('ANDROIDAPI', 8)
|
|
||||||
|
|
||||||
ap = argparse.ArgumentParser(description='''\
|
|
||||||
Package a Python application for Android.
|
|
||||||
|
|
||||||
For this to work, Java and Ant need to be in your path, as does the
|
|
||||||
tools directory of the Android SDK.
|
|
||||||
''')
|
|
||||||
|
|
||||||
ap.add_argument('--package', dest='package',
|
|
||||||
help=('The name of the java package the project will be'
|
|
||||||
' packaged under.'),
|
|
||||||
required=True)
|
|
||||||
ap.add_argument('--name', dest='name',
|
|
||||||
help=('The human-readable name of the project.'),
|
|
||||||
required=True)
|
|
||||||
ap.add_argument('--version', dest='version',
|
|
||||||
help=('The version number of the project. This should '
|
|
||||||
'consist of numbers and dots, and should have the '
|
|
||||||
'same number of groups of numbers as previous '
|
|
||||||
'versions.'),
|
|
||||||
required=True)
|
|
||||||
ap.add_argument('--numeric-version', dest='numeric_version',
|
|
||||||
help=('The numeric version number of the project. If not '
|
|
||||||
'given, this is automatically computed from the '
|
|
||||||
'version.'))
|
|
||||||
ap.add_argument('--dir', dest='dir',
|
|
||||||
help=('The directory containing public files for the '
|
|
||||||
'project.'))
|
|
||||||
ap.add_argument('--private', dest='private',
|
|
||||||
help=('The directory containing additional private files '
|
|
||||||
'for the project.'))
|
|
||||||
ap.add_argument('--launcher', dest='launcher', action='store_true',
|
|
||||||
help=('Provide this argument to build a multi-app '
|
|
||||||
'launcher, rather than a single app.'))
|
|
||||||
ap.add_argument('--icon-name', dest='icon_name',
|
|
||||||
help='The name of the project\'s launcher icon.')
|
|
||||||
ap.add_argument('--orientation', dest='orientation', default='landscape',
|
|
||||||
help=('The orientation that the game will display in. '
|
|
||||||
'Usually one of "landscape", "portrait" or '
|
|
||||||
'"sensor"'))
|
|
||||||
ap.add_argument('--permission', dest='permissions', action='append',
|
|
||||||
help='The permissions to give this app.', nargs='+')
|
|
||||||
ap.add_argument('--ignore-path', dest='ignore_path', action='append',
|
|
||||||
help='Ignore path when building the app')
|
|
||||||
ap.add_argument('--icon', dest='icon',
|
|
||||||
help='A png file to use as the icon for the application.')
|
|
||||||
ap.add_argument('--presplash', dest='presplash',
|
|
||||||
help=('A jpeg file to use as a screen while the '
|
|
||||||
'application is loading.'))
|
|
||||||
ap.add_argument('--ouya-category', dest='ouya_category',
|
|
||||||
help=('Valid values are GAME and APP. This must be '
|
|
||||||
'specified to enable OUYA console support.'))
|
|
||||||
ap.add_argument('--ouya-icon', dest='ouya_icon',
|
|
||||||
help=('A png file to use as the icon for the application '
|
|
||||||
'if it is installed on an OUYA console.'))
|
|
||||||
ap.add_argument('--install-location', dest='install_location',
|
|
||||||
default='auto',
|
|
||||||
help=('The default install location. Should be "auto", '
|
|
||||||
'"preferExternal" or "internalOnly".'))
|
|
||||||
ap.add_argument('--compile-pyo', dest='compile_pyo', action='store_true',
|
|
||||||
help=('Compile all .py files to .pyo, and only distribute '
|
|
||||||
'the compiled bytecode.'))
|
|
||||||
ap.add_argument('--intent-filters', dest='intent_filters',
|
|
||||||
help=('Add intent-filters xml rules to the '
|
|
||||||
'AndroidManifest.xml file. The argument is a '
|
|
||||||
'filename containing xml. The filename should be '
|
|
||||||
'located relative to the python-for-android '
|
|
||||||
'directory'))
|
|
||||||
ap.add_argument('--with-billing', dest='billing_pubkey',
|
|
||||||
help='If set, the billing service will be added')
|
|
||||||
ap.add_argument('--blacklist', dest='blacklist',
|
|
||||||
default=join(curdir, 'blacklist.txt'),
|
|
||||||
help=('Use a blacklist file to match unwanted file in '
|
|
||||||
'the final APK'))
|
|
||||||
ap.add_argument('--whitelist', dest='whitelist',
|
|
||||||
default=join(curdir, 'whitelist.txt'),
|
|
||||||
help=('Use a whitelist file to prevent blacklisting of '
|
|
||||||
'file in the final APK'))
|
|
||||||
ap.add_argument('--sdk', dest='sdk_version', default=android_api,
|
|
||||||
help='Android SDK version to use. Default to 8')
|
|
||||||
ap.add_argument('--minsdk', dest='min_sdk_version', default=android_api,
|
|
||||||
type=int,
|
|
||||||
help='Minimum Android SDK version to use. Default to 8')
|
|
||||||
ap.add_argument('--window', dest='window', action='store_true',
|
|
||||||
help='Indicate if the application will be windowed')
|
|
||||||
ap.add_argument('--wakelock', dest='wakelock', action='store_true',
|
|
||||||
help=('Indicate if the application needs the device '
|
|
||||||
'to stay on'))
|
|
||||||
ap.add_argument('command', nargs='*',
|
|
||||||
help=('The command to pass to ant (debug, release, '
|
|
||||||
'installd, installr)'))
|
|
||||||
ap.add_argument('--add-jar', dest='add_jar', action='append',
|
|
||||||
help=('Add a Java .jar to the libs, so you can access its '
|
|
||||||
'classes with pyjnius. You can specify this '
|
|
||||||
'argument more than once to include multiple jars'))
|
|
||||||
ap.add_argument('--meta-data', dest='meta_data', action='append',
|
|
||||||
help='Custom key=value to add in application metadata')
|
|
||||||
ap.add_argument('--resource', dest='resource', action='append',
|
|
||||||
help='Custom key=value to add in strings.xml resource file')
|
|
||||||
ap.add_argument('--manifest-extra', dest='manifest_extra', action='append',
|
|
||||||
help='Custom file to add at the end of the manifest')
|
|
||||||
|
|
||||||
if args is None:
|
|
||||||
args = sys.argv[1:]
|
|
||||||
args = ap.parse_args(args)
|
|
||||||
|
|
||||||
if args.name and args.name[0] == '"' and args.name[-1] == '"':
|
|
||||||
args.name = args.name[1:-1]
|
|
||||||
|
|
||||||
if not args.dir and not args.private and not args.launcher:
|
|
||||||
ap.error('One of --dir, --private, or --launcher must be supplied.')
|
|
||||||
|
|
||||||
if args.permissions is None:
|
|
||||||
args.permissions = []
|
|
||||||
elif args.permissions:
|
|
||||||
if isinstance(args.permissions[0], list):
|
|
||||||
args.permissions = [p for perm in args.permissions for p in perm]
|
|
||||||
|
|
||||||
if args.ignore_path is None:
|
|
||||||
args.ignore_path = []
|
|
||||||
|
|
||||||
if args.meta_data is None:
|
|
||||||
args.meta_data = []
|
|
||||||
|
|
||||||
if args.resource is None:
|
|
||||||
args.resource = []
|
|
||||||
|
|
||||||
if args.manifest_extra is None:
|
|
||||||
args.manifest_extra = []
|
|
||||||
|
|
||||||
if args.compile_pyo:
|
|
||||||
if PYTHON is None:
|
|
||||||
ap.error('To use --compile-pyo, you need Python 2.7.1 installed '
|
|
||||||
'and in your PATH.')
|
|
||||||
global BLACKLIST_PATTERNS
|
|
||||||
BLACKLIST_PATTERNS += ['*.py', '*.pyc']
|
|
||||||
|
|
||||||
if args.blacklist:
|
|
||||||
with open(args.blacklist) as fd:
|
|
||||||
patterns = [x.strip() for x in fd.read().splitlines() if x.strip()
|
|
||||||
and not x.startswith('#')]
|
|
||||||
BLACKLIST_PATTERNS += patterns
|
|
||||||
|
|
||||||
if args.whitelist:
|
|
||||||
with open(args.whitelist) as fd:
|
|
||||||
patterns = [x.strip() for x in fd.read().splitlines() if x.strip()
|
|
||||||
and not x.startswith('#')]
|
|
||||||
global WHITELIST_PATTERNS
|
|
||||||
WHITELIST_PATTERNS += patterns
|
|
||||||
|
|
||||||
make_package(args)
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
parse_args()
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,59 +0,0 @@
|
||||||
Metadata-Version: 1.0
|
|
||||||
Name: Jinja2
|
|
||||||
Version: 2.4.1
|
|
||||||
Summary: A small but fast and easy to use stand-alone template engine written in pure python.
|
|
||||||
Home-page: http://jinja.pocoo.org/
|
|
||||||
Author: Armin Ronacher
|
|
||||||
Author-email: armin.ronacher@active-4.com
|
|
||||||
License: BSD
|
|
||||||
Description:
|
|
||||||
Jinja2
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
Jinja2 is a template engine written in pure Python. It provides a
|
|
||||||
`Django`_ inspired non-XML syntax but supports inline expressions and
|
|
||||||
an optional `sandboxed`_ environment.
|
|
||||||
|
|
||||||
Nutshell
|
|
||||||
--------
|
|
||||||
|
|
||||||
Here a small example of a Jinja template::
|
|
||||||
|
|
||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Memberlist{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<ul>
|
|
||||||
{% for user in users %}
|
|
||||||
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
Philosophy
|
|
||||||
----------
|
|
||||||
|
|
||||||
Application logic is for the controller but don't try to make the life
|
|
||||||
for the template designer too hard by giving him too few functionality.
|
|
||||||
|
|
||||||
For more informations visit the new `Jinja2 webpage`_ and `documentation`_.
|
|
||||||
|
|
||||||
The `Jinja2 tip`_ is installable via `easy_install` with ``easy_install
|
|
||||||
Jinja2==dev``.
|
|
||||||
|
|
||||||
.. _sandboxed: http://en.wikipedia.org/wiki/Sandbox_(computer_security)
|
|
||||||
.. _Django: http://www.djangoproject.com/
|
|
||||||
.. _Jinja2 webpage: http://jinja.pocoo.org/
|
|
||||||
.. _documentation: http://jinja.pocoo.org/2/documentation/
|
|
||||||
.. _Jinja2 tip: http://dev.pocoo.org/hg/jinja2-main/archive/tip.tar.gz#egg=Jinja2-dev
|
|
||||||
|
|
||||||
Platform: UNKNOWN
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Environment :: Web Environment
|
|
||||||
Classifier: Intended Audience :: Developers
|
|
||||||
Classifier: License :: OSI Approved :: BSD License
|
|
||||||
Classifier: Operating System :: OS Independent
|
|
||||||
Classifier: Programming Language :: Python
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
||||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
||||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
|
@ -1,186 +0,0 @@
|
||||||
AUTHORS
|
|
||||||
CHANGES
|
|
||||||
LICENSE
|
|
||||||
MANIFEST.in
|
|
||||||
Makefile
|
|
||||||
setup.cfg
|
|
||||||
setup.py
|
|
||||||
Jinja2.egg-info/PKG-INFO
|
|
||||||
Jinja2.egg-info/SOURCES.txt
|
|
||||||
Jinja2.egg-info/dependency_links.txt
|
|
||||||
Jinja2.egg-info/entry_points.txt
|
|
||||||
Jinja2.egg-info/not-zip-safe
|
|
||||||
Jinja2.egg-info/requires.txt
|
|
||||||
Jinja2.egg-info/top_level.txt
|
|
||||||
artwork/jinjalogo.svg
|
|
||||||
custom_fixers/__init__.py
|
|
||||||
custom_fixers/fix_alt_unicode.py
|
|
||||||
custom_fixers/fix_broken_reraising.py
|
|
||||||
custom_fixers/fix_xrange2.py
|
|
||||||
docs/Makefile
|
|
||||||
docs/api.rst
|
|
||||||
docs/cache_extension.py
|
|
||||||
docs/changelog.rst
|
|
||||||
docs/conf.py
|
|
||||||
docs/extensions.rst
|
|
||||||
docs/faq.rst
|
|
||||||
docs/index.rst
|
|
||||||
docs/integration.rst
|
|
||||||
docs/intro.rst
|
|
||||||
docs/jinjaext.py
|
|
||||||
docs/jinjaext.pyc
|
|
||||||
docs/sandbox.rst
|
|
||||||
docs/switching.rst
|
|
||||||
docs/templates.rst
|
|
||||||
docs/tricks.rst
|
|
||||||
docs/_build/.ignore
|
|
||||||
docs/_build/html/.buildinfo
|
|
||||||
docs/_build/html/api.html
|
|
||||||
docs/_build/html/changelog.html
|
|
||||||
docs/_build/html/extensions.html
|
|
||||||
docs/_build/html/faq.html
|
|
||||||
docs/_build/html/genindex.html
|
|
||||||
docs/_build/html/index.html
|
|
||||||
docs/_build/html/integration.html
|
|
||||||
docs/_build/html/intro.html
|
|
||||||
docs/_build/html/objects.inv
|
|
||||||
docs/_build/html/sandbox.html
|
|
||||||
docs/_build/html/search.html
|
|
||||||
docs/_build/html/searchindex.js
|
|
||||||
docs/_build/html/switching.html
|
|
||||||
docs/_build/html/templates.html
|
|
||||||
docs/_build/html/tricks.html
|
|
||||||
docs/_build/html/_sources/api.txt
|
|
||||||
docs/_build/html/_sources/changelog.txt
|
|
||||||
docs/_build/html/_sources/extensions.txt
|
|
||||||
docs/_build/html/_sources/faq.txt
|
|
||||||
docs/_build/html/_sources/index.txt
|
|
||||||
docs/_build/html/_sources/integration.txt
|
|
||||||
docs/_build/html/_sources/intro.txt
|
|
||||||
docs/_build/html/_sources/sandbox.txt
|
|
||||||
docs/_build/html/_sources/switching.txt
|
|
||||||
docs/_build/html/_sources/templates.txt
|
|
||||||
docs/_build/html/_sources/tricks.txt
|
|
||||||
docs/_build/html/_static/basic.css
|
|
||||||
docs/_build/html/_static/darkmetal.png
|
|
||||||
docs/_build/html/_static/default.css
|
|
||||||
docs/_build/html/_static/doctools.js
|
|
||||||
docs/_build/html/_static/file.png
|
|
||||||
docs/_build/html/_static/headerbg.png
|
|
||||||
docs/_build/html/_static/implementation.png
|
|
||||||
docs/_build/html/_static/jinja.js
|
|
||||||
docs/_build/html/_static/jinjabanner.png
|
|
||||||
docs/_build/html/_static/jquery.js
|
|
||||||
docs/_build/html/_static/metal.png
|
|
||||||
docs/_build/html/_static/minus.png
|
|
||||||
docs/_build/html/_static/navigation.png
|
|
||||||
docs/_build/html/_static/note.png
|
|
||||||
docs/_build/html/_static/plus.png
|
|
||||||
docs/_build/html/_static/print.css
|
|
||||||
docs/_build/html/_static/pygments.css
|
|
||||||
docs/_build/html/_static/searchtools.js
|
|
||||||
docs/_build/html/_static/style.css
|
|
||||||
docs/_build/html/_static/underscore.js
|
|
||||||
docs/_build/html/_static/watermark.png
|
|
||||||
docs/_build/html/_static/watermark_blur.png
|
|
||||||
docs/_static/.ignore
|
|
||||||
docs/_static/darkmetal.png
|
|
||||||
docs/_static/headerbg.png
|
|
||||||
docs/_static/implementation.png
|
|
||||||
docs/_static/jinja.js
|
|
||||||
docs/_static/jinjabanner.png
|
|
||||||
docs/_static/metal.png
|
|
||||||
docs/_static/navigation.png
|
|
||||||
docs/_static/note.png
|
|
||||||
docs/_static/print.css
|
|
||||||
docs/_static/style.css
|
|
||||||
docs/_static/watermark.png
|
|
||||||
docs/_static/watermark_blur.png
|
|
||||||
docs/_templates/.ignore
|
|
||||||
docs/_templates/genindex.html
|
|
||||||
docs/_templates/layout.html
|
|
||||||
docs/_templates/opensearch.xml
|
|
||||||
docs/_templates/page.html
|
|
||||||
docs/_templates/search.html
|
|
||||||
examples/bench.py
|
|
||||||
examples/profile.py
|
|
||||||
examples/basic/cycle.py
|
|
||||||
examples/basic/debugger.py
|
|
||||||
examples/basic/inheritance.py
|
|
||||||
examples/basic/test.py
|
|
||||||
examples/basic/test_filter_and_linestatements.py
|
|
||||||
examples/basic/test_loop_filter.py
|
|
||||||
examples/basic/translate.py
|
|
||||||
examples/basic/templates/broken.html
|
|
||||||
examples/basic/templates/subbroken.html
|
|
||||||
examples/rwbench/djangoext.py
|
|
||||||
examples/rwbench/rwbench.py
|
|
||||||
examples/rwbench/django/_form.html
|
|
||||||
examples/rwbench/django/_input_field.html
|
|
||||||
examples/rwbench/django/_textarea.html
|
|
||||||
examples/rwbench/django/index.html
|
|
||||||
examples/rwbench/django/layout.html
|
|
||||||
examples/rwbench/genshi/helpers.html
|
|
||||||
examples/rwbench/genshi/index.html
|
|
||||||
examples/rwbench/genshi/layout.html
|
|
||||||
examples/rwbench/jinja/helpers.html
|
|
||||||
examples/rwbench/jinja/index.html
|
|
||||||
examples/rwbench/jinja/layout.html
|
|
||||||
examples/rwbench/mako/helpers.html
|
|
||||||
examples/rwbench/mako/index.html
|
|
||||||
examples/rwbench/mako/layout.html
|
|
||||||
ext/JinjaTemplates.tmbundle.tar.gz
|
|
||||||
ext/djangojinja2.py
|
|
||||||
ext/inlinegettext.py
|
|
||||||
ext/jinja.el
|
|
||||||
ext/Vim/htmljinja.vim
|
|
||||||
ext/Vim/jinja.vim
|
|
||||||
ext/django2jinja/django2jinja.py
|
|
||||||
ext/django2jinja/example.py
|
|
||||||
ext/django2jinja/templates/index.html
|
|
||||||
ext/django2jinja/templates/layout.html
|
|
||||||
ext/django2jinja/templates/subtemplate.html
|
|
||||||
jinja2/__init__.py
|
|
||||||
jinja2/_speedups.c
|
|
||||||
jinja2/_stringdefs.py
|
|
||||||
jinja2/bccache.py
|
|
||||||
jinja2/compiler.py
|
|
||||||
jinja2/constants.py
|
|
||||||
jinja2/debug.py
|
|
||||||
jinja2/defaults.py
|
|
||||||
jinja2/environment.py
|
|
||||||
jinja2/exceptions.py
|
|
||||||
jinja2/ext.py
|
|
||||||
jinja2/filters.py
|
|
||||||
jinja2/lexer.py
|
|
||||||
jinja2/loaders.py
|
|
||||||
jinja2/meta.py
|
|
||||||
jinja2/nodes.py
|
|
||||||
jinja2/optimizer.py
|
|
||||||
jinja2/parser.py
|
|
||||||
jinja2/runtime.py
|
|
||||||
jinja2/sandbox.py
|
|
||||||
jinja2/tests.py
|
|
||||||
jinja2/utils.py
|
|
||||||
jinja2/visitor.py
|
|
||||||
jinja2/testsuite/__init__.py
|
|
||||||
jinja2/testsuite/api.py
|
|
||||||
jinja2/testsuite/core_tags.py
|
|
||||||
jinja2/testsuite/debug.py
|
|
||||||
jinja2/testsuite/doctests.py
|
|
||||||
jinja2/testsuite/ext.py
|
|
||||||
jinja2/testsuite/filters.py
|
|
||||||
jinja2/testsuite/imports.py
|
|
||||||
jinja2/testsuite/inheritance.py
|
|
||||||
jinja2/testsuite/lexnparse.py
|
|
||||||
jinja2/testsuite/loader.py
|
|
||||||
jinja2/testsuite/regression.py
|
|
||||||
jinja2/testsuite/security.py
|
|
||||||
jinja2/testsuite/tests.py
|
|
||||||
jinja2/testsuite/utils.py
|
|
||||||
jinja2/testsuite/res/__init__.py
|
|
||||||
jinja2/testsuite/res/__init__.pyc
|
|
||||||
jinja2/testsuite/res/templates/broken.html
|
|
||||||
jinja2/testsuite/res/templates/syntaxerror.html
|
|
||||||
jinja2/testsuite/res/templates/test.html
|
|
||||||
jinja2/testsuite/res/templates/foo/test.html
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
[babel.extractors]
|
|
||||||
jinja2 = jinja2.ext:babel_extract[i18n]
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
[i18n]
|
|
||||||
Babel>=0.8
|
|
|
@ -1 +0,0 @@
|
||||||
jinja2
|
|
|
@ -1,73 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
Jinja2 is a template engine written in pure Python. It provides a
|
|
||||||
Django inspired non-XML syntax but supports inline expressions and
|
|
||||||
an optional sandboxed environment.
|
|
||||||
|
|
||||||
Nutshell
|
|
||||||
--------
|
|
||||||
|
|
||||||
Here a small example of a Jinja2 template::
|
|
||||||
|
|
||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Memberlist{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<ul>
|
|
||||||
{% for user in users %}
|
|
||||||
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
try:
|
|
||||||
__version__ = __import__('pkg_resources') \
|
|
||||||
.get_distribution('Jinja2').version
|
|
||||||
except:
|
|
||||||
__version__ = 'unknown'
|
|
||||||
|
|
||||||
# high level interface
|
|
||||||
from jinja2.environment import Environment, Template
|
|
||||||
|
|
||||||
# loaders
|
|
||||||
from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \
|
|
||||||
DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader, \
|
|
||||||
ModuleLoader
|
|
||||||
|
|
||||||
# bytecode caches
|
|
||||||
from jinja2.bccache import BytecodeCache, FileSystemBytecodeCache, \
|
|
||||||
MemcachedBytecodeCache
|
|
||||||
|
|
||||||
# undefined types
|
|
||||||
from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined
|
|
||||||
|
|
||||||
# exceptions
|
|
||||||
from jinja2.exceptions import TemplateError, UndefinedError, \
|
|
||||||
TemplateNotFound, TemplatesNotFound, TemplateSyntaxError, \
|
|
||||||
TemplateAssertionError
|
|
||||||
|
|
||||||
# decorators and public utilities
|
|
||||||
from jinja2.filters import environmentfilter, contextfilter, \
|
|
||||||
evalcontextfilter
|
|
||||||
from jinja2.utils import Markup, escape, clear_caches, \
|
|
||||||
environmentfunction, evalcontextfunction, contextfunction, \
|
|
||||||
is_undefined
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'Environment', 'Template', 'BaseLoader', 'FileSystemLoader',
|
|
||||||
'PackageLoader', 'DictLoader', 'FunctionLoader', 'PrefixLoader',
|
|
||||||
'ChoiceLoader', 'BytecodeCache', 'FileSystemBytecodeCache',
|
|
||||||
'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined',
|
|
||||||
'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound',
|
|
||||||
'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError',
|
|
||||||
'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape',
|
|
||||||
'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined',
|
|
||||||
'evalcontextfilter', 'evalcontextfunction'
|
|
||||||
]
|
|
|
@ -1,259 +0,0 @@
|
||||||
/**
|
|
||||||
* jinja2._speedups
|
|
||||||
* ~~~~~~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* This module implements functions for automatic escaping in C for better
|
|
||||||
* performance. Additionally it defines a `tb_set_next` function to patch the
|
|
||||||
* debug traceback. If the speedups module is not compiled a ctypes
|
|
||||||
* implementation of `tb_set_next` and Python implementations of the other
|
|
||||||
* functions are used.
|
|
||||||
*
|
|
||||||
* :copyright: (c) 2009 by the Jinja Team.
|
|
||||||
* :license: BSD.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Python.h>
|
|
||||||
|
|
||||||
#define ESCAPED_CHARS_TABLE_SIZE 63
|
|
||||||
#define UNICHR(x) (((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL))->str);
|
|
||||||
|
|
||||||
#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
|
|
||||||
typedef int Py_ssize_t;
|
|
||||||
#define PY_SSIZE_T_MAX INT_MAX
|
|
||||||
#define PY_SSIZE_T_MIN INT_MIN
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject* markup;
|
|
||||||
static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE];
|
|
||||||
static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE];
|
|
||||||
|
|
||||||
static int
|
|
||||||
init_constants(void)
|
|
||||||
{
|
|
||||||
PyObject *module;
|
|
||||||
/* happing of characters to replace */
|
|
||||||
escaped_chars_repl['"'] = UNICHR(""");
|
|
||||||
escaped_chars_repl['\''] = UNICHR("'");
|
|
||||||
escaped_chars_repl['&'] = UNICHR("&");
|
|
||||||
escaped_chars_repl['<'] = UNICHR("<");
|
|
||||||
escaped_chars_repl['>'] = UNICHR(">");
|
|
||||||
|
|
||||||
/* lengths of those characters when replaced - 1 */
|
|
||||||
memset(escaped_chars_delta_len, 0, sizeof (escaped_chars_delta_len));
|
|
||||||
escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \
|
|
||||||
escaped_chars_delta_len['&'] = 4;
|
|
||||||
escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3;
|
|
||||||
|
|
||||||
/* import markup type so that we can mark the return value */
|
|
||||||
module = PyImport_ImportModule("jinja2.utils");
|
|
||||||
if (!module)
|
|
||||||
return 0;
|
|
||||||
markup = PyObject_GetAttrString(module, "Markup");
|
|
||||||
Py_DECREF(module);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
escape_unicode(PyUnicodeObject *in)
|
|
||||||
{
|
|
||||||
PyUnicodeObject *out;
|
|
||||||
Py_UNICODE *inp = in->str;
|
|
||||||
const Py_UNICODE *inp_end = in->str + in->length;
|
|
||||||
Py_UNICODE *next_escp;
|
|
||||||
Py_UNICODE *outp;
|
|
||||||
Py_ssize_t delta=0, erepl=0, delta_len=0;
|
|
||||||
|
|
||||||
/* First we need to figure out how long the escaped string will be */
|
|
||||||
while (*(inp) || inp < inp_end) {
|
|
||||||
if (*inp < ESCAPED_CHARS_TABLE_SIZE && escaped_chars_delta_len[*inp]) {
|
|
||||||
delta += escaped_chars_delta_len[*inp];
|
|
||||||
++erepl;
|
|
||||||
}
|
|
||||||
++inp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Do we need to escape anything at all? */
|
|
||||||
if (!erepl) {
|
|
||||||
Py_INCREF(in);
|
|
||||||
return (PyObject*)in;
|
|
||||||
}
|
|
||||||
|
|
||||||
out = (PyUnicodeObject*)PyUnicode_FromUnicode(NULL, in->length + delta);
|
|
||||||
if (!out)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
outp = out->str;
|
|
||||||
inp = in->str;
|
|
||||||
while (erepl-- > 0) {
|
|
||||||
/* look for the next substitution */
|
|
||||||
next_escp = inp;
|
|
||||||
while (next_escp < inp_end) {
|
|
||||||
if (*next_escp < ESCAPED_CHARS_TABLE_SIZE &&
|
|
||||||
(delta_len = escaped_chars_delta_len[*next_escp])) {
|
|
||||||
++delta_len;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
++next_escp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next_escp > inp) {
|
|
||||||
/* copy unescaped chars between inp and next_escp */
|
|
||||||
Py_UNICODE_COPY(outp, inp, next_escp-inp);
|
|
||||||
outp += next_escp - inp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* escape 'next_escp' */
|
|
||||||
Py_UNICODE_COPY(outp, escaped_chars_repl[*next_escp], delta_len);
|
|
||||||
outp += delta_len;
|
|
||||||
|
|
||||||
inp = next_escp + 1;
|
|
||||||
}
|
|
||||||
if (inp < inp_end)
|
|
||||||
Py_UNICODE_COPY(outp, inp, in->length - (inp - in->str));
|
|
||||||
|
|
||||||
return (PyObject*)out;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
escape(PyObject *self, PyObject *text)
|
|
||||||
{
|
|
||||||
PyObject *s = NULL, *rv = NULL, *html;
|
|
||||||
|
|
||||||
/* we don't have to escape integers, bools or floats */
|
|
||||||
if (PyLong_CheckExact(text) ||
|
|
||||||
#if PY_MAJOR_VERSION < 3
|
|
||||||
PyInt_CheckExact(text) ||
|
|
||||||
#endif
|
|
||||||
PyFloat_CheckExact(text) || PyBool_Check(text) ||
|
|
||||||
text == Py_None)
|
|
||||||
return PyObject_CallFunctionObjArgs(markup, text, NULL);
|
|
||||||
|
|
||||||
/* if the object has an __html__ method that performs the escaping */
|
|
||||||
html = PyObject_GetAttrString(text, "__html__");
|
|
||||||
if (html) {
|
|
||||||
rv = PyObject_CallObject(html, NULL);
|
|
||||||
Py_DECREF(html);
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* otherwise make the object unicode if it isn't, then escape */
|
|
||||||
PyErr_Clear();
|
|
||||||
if (!PyUnicode_Check(text)) {
|
|
||||||
#if PY_MAJOR_VERSION < 3
|
|
||||||
PyObject *unicode = PyObject_Unicode(text);
|
|
||||||
#else
|
|
||||||
PyObject *unicode = PyObject_Str(text);
|
|
||||||
#endif
|
|
||||||
if (!unicode)
|
|
||||||
return NULL;
|
|
||||||
s = escape_unicode((PyUnicodeObject*)unicode);
|
|
||||||
Py_DECREF(unicode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
s = escape_unicode((PyUnicodeObject*)text);
|
|
||||||
|
|
||||||
/* convert the unicode string into a markup object. */
|
|
||||||
rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
|
|
||||||
Py_DECREF(s);
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
soft_unicode(PyObject *self, PyObject *s)
|
|
||||||
{
|
|
||||||
if (!PyUnicode_Check(s))
|
|
||||||
#if PY_MAJOR_VERSION < 3
|
|
||||||
return PyObject_Unicode(s);
|
|
||||||
#else
|
|
||||||
return PyObject_Str(s);
|
|
||||||
#endif
|
|
||||||
Py_INCREF(s);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
tb_set_next(PyObject *self, PyObject *args)
|
|
||||||
{
|
|
||||||
PyTracebackObject *tb, *old;
|
|
||||||
PyObject *next;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "O!O:tb_set_next", &PyTraceBack_Type, &tb, &next))
|
|
||||||
return NULL;
|
|
||||||
if (next == Py_None)
|
|
||||||
next = NULL;
|
|
||||||
else if (!PyTraceBack_Check(next)) {
|
|
||||||
PyErr_SetString(PyExc_TypeError,
|
|
||||||
"tb_set_next arg 2 must be traceback or None");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
Py_INCREF(next);
|
|
||||||
|
|
||||||
old = tb->tb_next;
|
|
||||||
tb->tb_next = (PyTracebackObject*)next;
|
|
||||||
Py_XDECREF(old);
|
|
||||||
|
|
||||||
Py_INCREF(Py_None);
|
|
||||||
return Py_None;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static PyMethodDef module_methods[] = {
|
|
||||||
{"escape", (PyCFunction)escape, METH_O,
|
|
||||||
"escape(s) -> markup\n\n"
|
|
||||||
"Convert the characters &, <, >, ', and \" in string s to HTML-safe\n"
|
|
||||||
"sequences. Use this if you need to display text that might contain\n"
|
|
||||||
"such characters in HTML. Marks return value as markup string."},
|
|
||||||
{"soft_unicode", (PyCFunction)soft_unicode, METH_O,
|
|
||||||
"soft_unicode(object) -> string\n\n"
|
|
||||||
"Make a string unicode if it isn't already. That way a markup\n"
|
|
||||||
"string is not converted back to unicode."},
|
|
||||||
{"tb_set_next", (PyCFunction)tb_set_next, METH_VARARGS,
|
|
||||||
"Set the tb_next member of a traceback object."},
|
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#if PY_MAJOR_VERSION < 3
|
|
||||||
|
|
||||||
#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
|
|
||||||
#define PyMODINIT_FUNC void
|
|
||||||
#endif
|
|
||||||
PyMODINIT_FUNC
|
|
||||||
init_speedups(void)
|
|
||||||
{
|
|
||||||
if (!init_constants())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Py_InitModule3("jinja2._speedups", module_methods, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#else /* Python 3.x module initialization */
|
|
||||||
|
|
||||||
static struct PyModuleDef module_definition = {
|
|
||||||
PyModuleDef_HEAD_INIT,
|
|
||||||
"jinja2._speedups",
|
|
||||||
NULL,
|
|
||||||
-1,
|
|
||||||
module_methods,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL
|
|
||||||
};
|
|
||||||
|
|
||||||
PyMODINIT_FUNC
|
|
||||||
PyInit__speedups(void)
|
|
||||||
{
|
|
||||||
if (!init_constants())
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
return PyModule_Create(&module_definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
File diff suppressed because one or more lines are too long
|
@ -1,280 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.bccache
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements the bytecode cache system Jinja is optionally
|
|
||||||
using. This is useful if you have very complex template situations and
|
|
||||||
the compiliation of all those templates slow down your application too
|
|
||||||
much.
|
|
||||||
|
|
||||||
Situations where this is useful are often forking web applications that
|
|
||||||
are initialized on the first request.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD.
|
|
||||||
"""
|
|
||||||
from os import path, listdir
|
|
||||||
import marshal
|
|
||||||
import tempfile
|
|
||||||
import cPickle as pickle
|
|
||||||
import fnmatch
|
|
||||||
from cStringIO import StringIO
|
|
||||||
try:
|
|
||||||
from hashlib import sha1
|
|
||||||
except ImportError:
|
|
||||||
from sha import new as sha1
|
|
||||||
from jinja2.utils import open_if_exists
|
|
||||||
|
|
||||||
|
|
||||||
bc_version = 1
|
|
||||||
bc_magic = 'j2'.encode('ascii') + pickle.dumps(bc_version, 2)
|
|
||||||
|
|
||||||
|
|
||||||
class Bucket(object):
|
|
||||||
"""Buckets are used to store the bytecode for one template. It's created
|
|
||||||
and initialized by the bytecode cache and passed to the loading functions.
|
|
||||||
|
|
||||||
The buckets get an internal checksum from the cache assigned and use this
|
|
||||||
to automatically reject outdated cache material. Individual bytecode
|
|
||||||
cache subclasses don't have to care about cache invalidation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, environment, key, checksum):
|
|
||||||
self.environment = environment
|
|
||||||
self.key = key
|
|
||||||
self.checksum = checksum
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Resets the bucket (unloads the bytecode)."""
|
|
||||||
self.code = None
|
|
||||||
|
|
||||||
def load_bytecode(self, f):
|
|
||||||
"""Loads bytecode from a file or file like object."""
|
|
||||||
# make sure the magic header is correct
|
|
||||||
magic = f.read(len(bc_magic))
|
|
||||||
if magic != bc_magic:
|
|
||||||
self.reset()
|
|
||||||
return
|
|
||||||
# the source code of the file changed, we need to reload
|
|
||||||
checksum = pickle.load(f)
|
|
||||||
if self.checksum != checksum:
|
|
||||||
self.reset()
|
|
||||||
return
|
|
||||||
# now load the code. Because marshal is not able to load
|
|
||||||
# from arbitrary streams we have to work around that
|
|
||||||
if isinstance(f, file):
|
|
||||||
self.code = marshal.load(f)
|
|
||||||
else:
|
|
||||||
self.code = marshal.loads(f.read())
|
|
||||||
|
|
||||||
def write_bytecode(self, f):
|
|
||||||
"""Dump the bytecode into the file or file like object passed."""
|
|
||||||
if self.code is None:
|
|
||||||
raise TypeError('can\'t write empty bucket')
|
|
||||||
f.write(bc_magic)
|
|
||||||
pickle.dump(self.checksum, f, 2)
|
|
||||||
if isinstance(f, file):
|
|
||||||
marshal.dump(self.code, f)
|
|
||||||
else:
|
|
||||||
f.write(marshal.dumps(self.code))
|
|
||||||
|
|
||||||
def bytecode_from_string(self, string):
|
|
||||||
"""Load bytecode from a string."""
|
|
||||||
self.load_bytecode(StringIO(string))
|
|
||||||
|
|
||||||
def bytecode_to_string(self):
|
|
||||||
"""Return the bytecode as string."""
|
|
||||||
out = StringIO()
|
|
||||||
self.write_bytecode(out)
|
|
||||||
return out.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
class BytecodeCache(object):
|
|
||||||
"""To implement your own bytecode cache you have to subclass this class
|
|
||||||
and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
|
|
||||||
these methods are passed a :class:`~jinja2.bccache.Bucket`.
|
|
||||||
|
|
||||||
A very basic bytecode cache that saves the bytecode on the file system::
|
|
||||||
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
class MyCache(BytecodeCache):
|
|
||||||
|
|
||||||
def __init__(self, directory):
|
|
||||||
self.directory = directory
|
|
||||||
|
|
||||||
def load_bytecode(self, bucket):
|
|
||||||
filename = path.join(self.directory, bucket.key)
|
|
||||||
if path.exists(filename):
|
|
||||||
with open(filename, 'rb') as f:
|
|
||||||
bucket.load_bytecode(f)
|
|
||||||
|
|
||||||
def dump_bytecode(self, bucket):
|
|
||||||
filename = path.join(self.directory, bucket.key)
|
|
||||||
with open(filename, 'wb') as f:
|
|
||||||
bucket.write_bytecode(f)
|
|
||||||
|
|
||||||
A more advanced version of a filesystem based bytecode cache is part of
|
|
||||||
Jinja2.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def load_bytecode(self, bucket):
|
|
||||||
"""Subclasses have to override this method to load bytecode into a
|
|
||||||
bucket. If they are not able to find code in the cache for the
|
|
||||||
bucket, it must not do anything.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def dump_bytecode(self, bucket):
|
|
||||||
"""Subclasses have to override this method to write the bytecode
|
|
||||||
from a bucket back to the cache. If it unable to do so it must not
|
|
||||||
fail silently but raise an exception.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Clears the cache. This method is not used by Jinja2 but should be
|
|
||||||
implemented to allow applications to clear the bytecode cache used
|
|
||||||
by a particular environment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_cache_key(self, name, filename=None):
|
|
||||||
"""Returns the unique hash key for this template name."""
|
|
||||||
hash = sha1(name.encode('utf-8'))
|
|
||||||
if filename is not None:
|
|
||||||
if isinstance(filename, unicode):
|
|
||||||
filename = filename.encode('utf-8')
|
|
||||||
hash.update('|' + filename)
|
|
||||||
return hash.hexdigest()
|
|
||||||
|
|
||||||
def get_source_checksum(self, source):
|
|
||||||
"""Returns a checksum for the source."""
|
|
||||||
return sha1(source.encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
def get_bucket(self, environment, name, filename, source):
|
|
||||||
"""Return a cache bucket for the given template. All arguments are
|
|
||||||
mandatory but filename may be `None`.
|
|
||||||
"""
|
|
||||||
key = self.get_cache_key(name, filename)
|
|
||||||
checksum = self.get_source_checksum(source)
|
|
||||||
bucket = Bucket(environment, key, checksum)
|
|
||||||
self.load_bytecode(bucket)
|
|
||||||
return bucket
|
|
||||||
|
|
||||||
def set_bucket(self, bucket):
|
|
||||||
"""Put the bucket into the cache."""
|
|
||||||
self.dump_bytecode(bucket)
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystemBytecodeCache(BytecodeCache):
|
|
||||||
"""A bytecode cache that stores bytecode on the filesystem. It accepts
|
|
||||||
two arguments: The directory where the cache items are stored and a
|
|
||||||
pattern string that is used to build the filename.
|
|
||||||
|
|
||||||
If no directory is specified the system temporary items folder is used.
|
|
||||||
|
|
||||||
The pattern can be used to have multiple separate caches operate on the
|
|
||||||
same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s``
|
|
||||||
is replaced with the cache key.
|
|
||||||
|
|
||||||
>>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache')
|
|
||||||
|
|
||||||
This bytecode cache supports clearing of the cache using the clear method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, directory=None, pattern='__jinja2_%s.cache'):
|
|
||||||
if directory is None:
|
|
||||||
directory = tempfile.gettempdir()
|
|
||||||
self.directory = directory
|
|
||||||
self.pattern = pattern
|
|
||||||
|
|
||||||
def _get_cache_filename(self, bucket):
|
|
||||||
return path.join(self.directory, self.pattern % bucket.key)
|
|
||||||
|
|
||||||
def load_bytecode(self, bucket):
|
|
||||||
f = open_if_exists(self._get_cache_filename(bucket), 'rb')
|
|
||||||
if f is not None:
|
|
||||||
try:
|
|
||||||
bucket.load_bytecode(f)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def dump_bytecode(self, bucket):
|
|
||||||
f = open(self._get_cache_filename(bucket), 'wb')
|
|
||||||
try:
|
|
||||||
bucket.write_bytecode(f)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
# imported lazily here because google app-engine doesn't support
|
|
||||||
# write access on the file system and the function does not exist
|
|
||||||
# normally.
|
|
||||||
from os import remove
|
|
||||||
files = fnmatch.filter(listdir(self.directory), self.pattern % '*')
|
|
||||||
for filename in files:
|
|
||||||
try:
|
|
||||||
remove(path.join(self.directory, filename))
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MemcachedBytecodeCache(BytecodeCache):
|
|
||||||
"""This class implements a bytecode cache that uses a memcache cache for
|
|
||||||
storing the information. It does not enforce a specific memcache library
|
|
||||||
(tummy's memcache or cmemcache) but will accept any class that provides
|
|
||||||
the minimal interface required.
|
|
||||||
|
|
||||||
Libraries compatible with this class:
|
|
||||||
|
|
||||||
- `werkzeug <http://werkzeug.pocoo.org/>`_.contrib.cache
|
|
||||||
- `python-memcached <http://www.tummy.com/Community/software/python-memcached/>`_
|
|
||||||
- `cmemcache <http://gijsbert.org/cmemcache/>`_
|
|
||||||
|
|
||||||
(Unfortunately the django cache interface is not compatible because it
|
|
||||||
does not support storing binary data, only unicode. You can however pass
|
|
||||||
the underlying cache client to the bytecode cache which is available
|
|
||||||
as `django.core.cache.cache._client`.)
|
|
||||||
|
|
||||||
The minimal interface for the client passed to the constructor is this:
|
|
||||||
|
|
||||||
.. class:: MinimalClientInterface
|
|
||||||
|
|
||||||
.. method:: set(key, value[, timeout])
|
|
||||||
|
|
||||||
Stores the bytecode in the cache. `value` is a string and
|
|
||||||
`timeout` the timeout of the key. If timeout is not provided
|
|
||||||
a default timeout or no timeout should be assumed, if it's
|
|
||||||
provided it's an integer with the number of seconds the cache
|
|
||||||
item should exist.
|
|
||||||
|
|
||||||
.. method:: get(key)
|
|
||||||
|
|
||||||
Returns the value for the cache key. If the item does not
|
|
||||||
exist in the cache the return value must be `None`.
|
|
||||||
|
|
||||||
The other arguments to the constructor are the prefix for all keys that
|
|
||||||
is added before the actual cache key and the timeout for the bytecode in
|
|
||||||
the cache system. We recommend a high (or no) timeout.
|
|
||||||
|
|
||||||
This bytecode cache does not support clearing of used items in the cache.
|
|
||||||
The clear method is a no-operation function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, client, prefix='jinja2/bytecode/', timeout=None):
|
|
||||||
self.client = client
|
|
||||||
self.prefix = prefix
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
def load_bytecode(self, bucket):
|
|
||||||
code = self.client.get(self.prefix + bucket.key)
|
|
||||||
if code is not None:
|
|
||||||
bucket.bytecode_from_string(code)
|
|
||||||
|
|
||||||
def dump_bytecode(self, bucket):
|
|
||||||
args = (self.prefix + bucket.key, bucket.bytecode_to_string())
|
|
||||||
if self.timeout is not None:
|
|
||||||
args += (self.timeout,)
|
|
||||||
self.client.set(*args)
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,290 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.constants
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Various constants.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
#: list of lorem ipsum words used by the lipsum() helper function
|
|
||||||
LOREM_IPSUM_WORDS = u'''\
|
|
||||||
a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
|
|
||||||
auctor augue bibendum blandit class commodo condimentum congue consectetuer
|
|
||||||
consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus
|
|
||||||
diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend
|
|
||||||
elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames
|
|
||||||
faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac
|
|
||||||
hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum
|
|
||||||
justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem
|
|
||||||
luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie
|
|
||||||
mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non
|
|
||||||
nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque
|
|
||||||
penatibus per pharetra phasellus placerat platea porta porttitor posuere
|
|
||||||
potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus
|
|
||||||
ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit
|
|
||||||
sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor
|
|
||||||
tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices
|
|
||||||
ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus
|
|
||||||
viverra volutpat vulputate'''
|
|
||||||
|
|
||||||
|
|
||||||
#: a dict of all html entities + apos
|
|
||||||
HTML_ENTITIES = {
|
|
||||||
'AElig': 198,
|
|
||||||
'Aacute': 193,
|
|
||||||
'Acirc': 194,
|
|
||||||
'Agrave': 192,
|
|
||||||
'Alpha': 913,
|
|
||||||
'Aring': 197,
|
|
||||||
'Atilde': 195,
|
|
||||||
'Auml': 196,
|
|
||||||
'Beta': 914,
|
|
||||||
'Ccedil': 199,
|
|
||||||
'Chi': 935,
|
|
||||||
'Dagger': 8225,
|
|
||||||
'Delta': 916,
|
|
||||||
'ETH': 208,
|
|
||||||
'Eacute': 201,
|
|
||||||
'Ecirc': 202,
|
|
||||||
'Egrave': 200,
|
|
||||||
'Epsilon': 917,
|
|
||||||
'Eta': 919,
|
|
||||||
'Euml': 203,
|
|
||||||
'Gamma': 915,
|
|
||||||
'Iacute': 205,
|
|
||||||
'Icirc': 206,
|
|
||||||
'Igrave': 204,
|
|
||||||
'Iota': 921,
|
|
||||||
'Iuml': 207,
|
|
||||||
'Kappa': 922,
|
|
||||||
'Lambda': 923,
|
|
||||||
'Mu': 924,
|
|
||||||
'Ntilde': 209,
|
|
||||||
'Nu': 925,
|
|
||||||
'OElig': 338,
|
|
||||||
'Oacute': 211,
|
|
||||||
'Ocirc': 212,
|
|
||||||
'Ograve': 210,
|
|
||||||
'Omega': 937,
|
|
||||||
'Omicron': 927,
|
|
||||||
'Oslash': 216,
|
|
||||||
'Otilde': 213,
|
|
||||||
'Ouml': 214,
|
|
||||||
'Phi': 934,
|
|
||||||
'Pi': 928,
|
|
||||||
'Prime': 8243,
|
|
||||||
'Psi': 936,
|
|
||||||
'Rho': 929,
|
|
||||||
'Scaron': 352,
|
|
||||||
'Sigma': 931,
|
|
||||||
'THORN': 222,
|
|
||||||
'Tau': 932,
|
|
||||||
'Theta': 920,
|
|
||||||
'Uacute': 218,
|
|
||||||
'Ucirc': 219,
|
|
||||||
'Ugrave': 217,
|
|
||||||
'Upsilon': 933,
|
|
||||||
'Uuml': 220,
|
|
||||||
'Xi': 926,
|
|
||||||
'Yacute': 221,
|
|
||||||
'Yuml': 376,
|
|
||||||
'Zeta': 918,
|
|
||||||
'aacute': 225,
|
|
||||||
'acirc': 226,
|
|
||||||
'acute': 180,
|
|
||||||
'aelig': 230,
|
|
||||||
'agrave': 224,
|
|
||||||
'alefsym': 8501,
|
|
||||||
'alpha': 945,
|
|
||||||
'amp': 38,
|
|
||||||
'and': 8743,
|
|
||||||
'ang': 8736,
|
|
||||||
'apos': 39,
|
|
||||||
'aring': 229,
|
|
||||||
'asymp': 8776,
|
|
||||||
'atilde': 227,
|
|
||||||
'auml': 228,
|
|
||||||
'bdquo': 8222,
|
|
||||||
'beta': 946,
|
|
||||||
'brvbar': 166,
|
|
||||||
'bull': 8226,
|
|
||||||
'cap': 8745,
|
|
||||||
'ccedil': 231,
|
|
||||||
'cedil': 184,
|
|
||||||
'cent': 162,
|
|
||||||
'chi': 967,
|
|
||||||
'circ': 710,
|
|
||||||
'clubs': 9827,
|
|
||||||
'cong': 8773,
|
|
||||||
'copy': 169,
|
|
||||||
'crarr': 8629,
|
|
||||||
'cup': 8746,
|
|
||||||
'curren': 164,
|
|
||||||
'dArr': 8659,
|
|
||||||
'dagger': 8224,
|
|
||||||
'darr': 8595,
|
|
||||||
'deg': 176,
|
|
||||||
'delta': 948,
|
|
||||||
'diams': 9830,
|
|
||||||
'divide': 247,
|
|
||||||
'eacute': 233,
|
|
||||||
'ecirc': 234,
|
|
||||||
'egrave': 232,
|
|
||||||
'empty': 8709,
|
|
||||||
'emsp': 8195,
|
|
||||||
'ensp': 8194,
|
|
||||||
'epsilon': 949,
|
|
||||||
'equiv': 8801,
|
|
||||||
'eta': 951,
|
|
||||||
'eth': 240,
|
|
||||||
'euml': 235,
|
|
||||||
'euro': 8364,
|
|
||||||
'exist': 8707,
|
|
||||||
'fnof': 402,
|
|
||||||
'forall': 8704,
|
|
||||||
'frac12': 189,
|
|
||||||
'frac14': 188,
|
|
||||||
'frac34': 190,
|
|
||||||
'frasl': 8260,
|
|
||||||
'gamma': 947,
|
|
||||||
'ge': 8805,
|
|
||||||
'gt': 62,
|
|
||||||
'hArr': 8660,
|
|
||||||
'harr': 8596,
|
|
||||||
'hearts': 9829,
|
|
||||||
'hellip': 8230,
|
|
||||||
'iacute': 237,
|
|
||||||
'icirc': 238,
|
|
||||||
'iexcl': 161,
|
|
||||||
'igrave': 236,
|
|
||||||
'image': 8465,
|
|
||||||
'infin': 8734,
|
|
||||||
'int': 8747,
|
|
||||||
'iota': 953,
|
|
||||||
'iquest': 191,
|
|
||||||
'isin': 8712,
|
|
||||||
'iuml': 239,
|
|
||||||
'kappa': 954,
|
|
||||||
'lArr': 8656,
|
|
||||||
'lambda': 955,
|
|
||||||
'lang': 9001,
|
|
||||||
'laquo': 171,
|
|
||||||
'larr': 8592,
|
|
||||||
'lceil': 8968,
|
|
||||||
'ldquo': 8220,
|
|
||||||
'le': 8804,
|
|
||||||
'lfloor': 8970,
|
|
||||||
'lowast': 8727,
|
|
||||||
'loz': 9674,
|
|
||||||
'lrm': 8206,
|
|
||||||
'lsaquo': 8249,
|
|
||||||
'lsquo': 8216,
|
|
||||||
'lt': 60,
|
|
||||||
'macr': 175,
|
|
||||||
'mdash': 8212,
|
|
||||||
'micro': 181,
|
|
||||||
'middot': 183,
|
|
||||||
'minus': 8722,
|
|
||||||
'mu': 956,
|
|
||||||
'nabla': 8711,
|
|
||||||
'nbsp': 160,
|
|
||||||
'ndash': 8211,
|
|
||||||
'ne': 8800,
|
|
||||||
'ni': 8715,
|
|
||||||
'not': 172,
|
|
||||||
'notin': 8713,
|
|
||||||
'nsub': 8836,
|
|
||||||
'ntilde': 241,
|
|
||||||
'nu': 957,
|
|
||||||
'oacute': 243,
|
|
||||||
'ocirc': 244,
|
|
||||||
'oelig': 339,
|
|
||||||
'ograve': 242,
|
|
||||||
'oline': 8254,
|
|
||||||
'omega': 969,
|
|
||||||
'omicron': 959,
|
|
||||||
'oplus': 8853,
|
|
||||||
'or': 8744,
|
|
||||||
'ordf': 170,
|
|
||||||
'ordm': 186,
|
|
||||||
'oslash': 248,
|
|
||||||
'otilde': 245,
|
|
||||||
'otimes': 8855,
|
|
||||||
'ouml': 246,
|
|
||||||
'para': 182,
|
|
||||||
'part': 8706,
|
|
||||||
'permil': 8240,
|
|
||||||
'perp': 8869,
|
|
||||||
'phi': 966,
|
|
||||||
'pi': 960,
|
|
||||||
'piv': 982,
|
|
||||||
'plusmn': 177,
|
|
||||||
'pound': 163,
|
|
||||||
'prime': 8242,
|
|
||||||
'prod': 8719,
|
|
||||||
'prop': 8733,
|
|
||||||
'psi': 968,
|
|
||||||
'quot': 34,
|
|
||||||
'rArr': 8658,
|
|
||||||
'radic': 8730,
|
|
||||||
'rang': 9002,
|
|
||||||
'raquo': 187,
|
|
||||||
'rarr': 8594,
|
|
||||||
'rceil': 8969,
|
|
||||||
'rdquo': 8221,
|
|
||||||
'real': 8476,
|
|
||||||
'reg': 174,
|
|
||||||
'rfloor': 8971,
|
|
||||||
'rho': 961,
|
|
||||||
'rlm': 8207,
|
|
||||||
'rsaquo': 8250,
|
|
||||||
'rsquo': 8217,
|
|
||||||
'sbquo': 8218,
|
|
||||||
'scaron': 353,
|
|
||||||
'sdot': 8901,
|
|
||||||
'sect': 167,
|
|
||||||
'shy': 173,
|
|
||||||
'sigma': 963,
|
|
||||||
'sigmaf': 962,
|
|
||||||
'sim': 8764,
|
|
||||||
'spades': 9824,
|
|
||||||
'sub': 8834,
|
|
||||||
'sube': 8838,
|
|
||||||
'sum': 8721,
|
|
||||||
'sup': 8835,
|
|
||||||
'sup1': 185,
|
|
||||||
'sup2': 178,
|
|
||||||
'sup3': 179,
|
|
||||||
'supe': 8839,
|
|
||||||
'szlig': 223,
|
|
||||||
'tau': 964,
|
|
||||||
'there4': 8756,
|
|
||||||
'theta': 952,
|
|
||||||
'thetasym': 977,
|
|
||||||
'thinsp': 8201,
|
|
||||||
'thorn': 254,
|
|
||||||
'tilde': 732,
|
|
||||||
'times': 215,
|
|
||||||
'trade': 8482,
|
|
||||||
'uArr': 8657,
|
|
||||||
'uacute': 250,
|
|
||||||
'uarr': 8593,
|
|
||||||
'ucirc': 251,
|
|
||||||
'ugrave': 249,
|
|
||||||
'uml': 168,
|
|
||||||
'upsih': 978,
|
|
||||||
'upsilon': 965,
|
|
||||||
'uuml': 252,
|
|
||||||
'weierp': 8472,
|
|
||||||
'xi': 958,
|
|
||||||
'yacute': 253,
|
|
||||||
'yen': 165,
|
|
||||||
'yuml': 255,
|
|
||||||
'zeta': 950,
|
|
||||||
'zwj': 8205,
|
|
||||||
'zwnj': 8204
|
|
||||||
}
|
|
|
@ -1,308 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.debug
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements the debug interface for Jinja. This module does some pretty
|
|
||||||
ugly stuff with the Python traceback system in order to achieve tracebacks
|
|
||||||
with correct line numbers, locals and contents.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
from jinja2.utils import CodeType, missing, internal_code
|
|
||||||
from jinja2.exceptions import TemplateSyntaxError
|
|
||||||
|
|
||||||
|
|
||||||
# how does the raise helper look like?
|
|
||||||
try:
|
|
||||||
exec("raise TypeError, 'foo'")
|
|
||||||
except SyntaxError:
|
|
||||||
raise_helper = 'raise __jinja_exception__[1]'
|
|
||||||
except TypeError:
|
|
||||||
raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]'
|
|
||||||
|
|
||||||
|
|
||||||
class TracebackFrameProxy(object):
|
|
||||||
"""Proxies a traceback frame."""
|
|
||||||
|
|
||||||
def __init__(self, tb):
|
|
||||||
self.tb = tb
|
|
||||||
|
|
||||||
def _set_tb_next(self, next):
|
|
||||||
if tb_set_next is not None:
|
|
||||||
tb_set_next(self.tb, next and next.tb or None)
|
|
||||||
self._tb_next = next
|
|
||||||
|
|
||||||
def _get_tb_next(self):
|
|
||||||
return self._tb_next
|
|
||||||
|
|
||||||
tb_next = property(_get_tb_next, _set_tb_next)
|
|
||||||
del _get_tb_next, _set_tb_next
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_jinja_frame(self):
|
|
||||||
return '__jinja_template__' in self.tb.tb_frame.f_globals
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(self.tb, name)
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessedTraceback(object):
|
|
||||||
"""Holds a Jinja preprocessed traceback for priting or reraising."""
|
|
||||||
|
|
||||||
def __init__(self, exc_type, exc_value, frames):
|
|
||||||
assert frames, 'no frames for this traceback?'
|
|
||||||
self.exc_type = exc_type
|
|
||||||
self.exc_value = exc_value
|
|
||||||
self.frames = frames
|
|
||||||
|
|
||||||
def chain_frames(self):
|
|
||||||
"""Chains the frames. Requires ctypes or the speedups extension."""
|
|
||||||
prev_tb = None
|
|
||||||
for tb in self.frames:
|
|
||||||
if prev_tb is not None:
|
|
||||||
prev_tb.tb_next = tb
|
|
||||||
prev_tb = tb
|
|
||||||
prev_tb.tb_next = None
|
|
||||||
|
|
||||||
def render_as_text(self, limit=None):
|
|
||||||
"""Return a string with the traceback."""
|
|
||||||
lines = traceback.format_exception(self.exc_type, self.exc_value,
|
|
||||||
self.frames[0], limit=limit)
|
|
||||||
return ''.join(lines).rstrip()
|
|
||||||
|
|
||||||
def render_as_html(self, full=False):
|
|
||||||
"""Return a unicode string with the traceback as rendered HTML."""
|
|
||||||
from jinja2.debugrenderer import render_traceback
|
|
||||||
return u'%s\n\n<!--\n%s\n-->' % (
|
|
||||||
render_traceback(self, full=full),
|
|
||||||
self.render_as_text().decode('utf-8', 'replace')
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_template_syntax_error(self):
|
|
||||||
"""`True` if this is a template syntax error."""
|
|
||||||
return isinstance(self.exc_value, TemplateSyntaxError)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exc_info(self):
|
|
||||||
"""Exception info tuple with a proxy around the frame objects."""
|
|
||||||
return self.exc_type, self.exc_value, self.frames[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def standard_exc_info(self):
|
|
||||||
"""Standard python exc_info for re-raising"""
|
|
||||||
return self.exc_type, self.exc_value, self.frames[0].tb
|
|
||||||
|
|
||||||
|
|
||||||
def make_traceback(exc_info, source_hint=None):
|
|
||||||
"""Creates a processed traceback object from the exc_info."""
|
|
||||||
exc_type, exc_value, tb = exc_info
|
|
||||||
if isinstance(exc_value, TemplateSyntaxError):
|
|
||||||
exc_info = translate_syntax_error(exc_value, source_hint)
|
|
||||||
initial_skip = 0
|
|
||||||
else:
|
|
||||||
initial_skip = 1
|
|
||||||
return translate_exception(exc_info, initial_skip)
|
|
||||||
|
|
||||||
|
|
||||||
def translate_syntax_error(error, source=None):
|
|
||||||
"""Rewrites a syntax error to please traceback systems."""
|
|
||||||
error.source = source
|
|
||||||
error.translated = True
|
|
||||||
exc_info = (error.__class__, error, None)
|
|
||||||
filename = error.filename
|
|
||||||
if filename is None:
|
|
||||||
filename = '<unknown>'
|
|
||||||
return fake_exc_info(exc_info, filename, error.lineno)
|
|
||||||
|
|
||||||
|
|
||||||
def translate_exception(exc_info, initial_skip=0):
|
|
||||||
"""If passed an exc_info it will automatically rewrite the exceptions
|
|
||||||
all the way down to the correct line numbers and frames.
|
|
||||||
"""
|
|
||||||
tb = exc_info[2]
|
|
||||||
frames = []
|
|
||||||
|
|
||||||
# skip some internal frames if wanted
|
|
||||||
for x in xrange(initial_skip):
|
|
||||||
if tb is not None:
|
|
||||||
tb = tb.tb_next
|
|
||||||
initial_tb = tb
|
|
||||||
|
|
||||||
while tb is not None:
|
|
||||||
# skip frames decorated with @internalcode. These are internal
|
|
||||||
# calls we can't avoid and that are useless in template debugging
|
|
||||||
# output.
|
|
||||||
if tb.tb_frame.f_code in internal_code:
|
|
||||||
tb = tb.tb_next
|
|
||||||
continue
|
|
||||||
|
|
||||||
# save a reference to the next frame if we override the current
|
|
||||||
# one with a faked one.
|
|
||||||
next = tb.tb_next
|
|
||||||
|
|
||||||
# fake template exceptions
|
|
||||||
template = tb.tb_frame.f_globals.get('__jinja_template__')
|
|
||||||
if template is not None:
|
|
||||||
lineno = template.get_corresponding_lineno(tb.tb_lineno)
|
|
||||||
tb = fake_exc_info(exc_info[:2] + (tb,), template.filename,
|
|
||||||
lineno)[2]
|
|
||||||
|
|
||||||
frames.append(TracebackFrameProxy(tb))
|
|
||||||
tb = next
|
|
||||||
|
|
||||||
# if we don't have any exceptions in the frames left, we have to
|
|
||||||
# reraise it unchanged.
|
|
||||||
# XXX: can we backup here? when could this happen?
|
|
||||||
if not frames:
|
|
||||||
raise exc_info[0], exc_info[1], exc_info[2]
|
|
||||||
|
|
||||||
traceback = ProcessedTraceback(exc_info[0], exc_info[1], frames)
|
|
||||||
if tb_set_next is not None:
|
|
||||||
traceback.chain_frames()
|
|
||||||
return traceback
|
|
||||||
|
|
||||||
|
|
||||||
def fake_exc_info(exc_info, filename, lineno):
|
|
||||||
"""Helper for `translate_exception`."""
|
|
||||||
exc_type, exc_value, tb = exc_info
|
|
||||||
|
|
||||||
# figure the real context out
|
|
||||||
if tb is not None:
|
|
||||||
real_locals = tb.tb_frame.f_locals.copy()
|
|
||||||
ctx = real_locals.get('context')
|
|
||||||
if ctx:
|
|
||||||
locals = ctx.get_all()
|
|
||||||
else:
|
|
||||||
locals = {}
|
|
||||||
for name, value in real_locals.iteritems():
|
|
||||||
if name.startswith('l_') and value is not missing:
|
|
||||||
locals[name[2:]] = value
|
|
||||||
|
|
||||||
# if there is a local called __jinja_exception__, we get
|
|
||||||
# rid of it to not break the debug functionality.
|
|
||||||
locals.pop('__jinja_exception__', None)
|
|
||||||
else:
|
|
||||||
locals = {}
|
|
||||||
|
|
||||||
# assamble fake globals we need
|
|
||||||
globals = {
|
|
||||||
'__name__': filename,
|
|
||||||
'__file__': filename,
|
|
||||||
'__jinja_exception__': exc_info[:2],
|
|
||||||
|
|
||||||
# we don't want to keep the reference to the template around
|
|
||||||
# to not cause circular dependencies, but we mark it as Jinja
|
|
||||||
# frame for the ProcessedTraceback
|
|
||||||
'__jinja_template__': None
|
|
||||||
}
|
|
||||||
|
|
||||||
# and fake the exception
|
|
||||||
code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec')
|
|
||||||
|
|
||||||
# if it's possible, change the name of the code. This won't work
|
|
||||||
# on some python environments such as google appengine
|
|
||||||
try:
|
|
||||||
if tb is None:
|
|
||||||
location = 'template'
|
|
||||||
else:
|
|
||||||
function = tb.tb_frame.f_code.co_name
|
|
||||||
if function == 'root':
|
|
||||||
location = 'top-level template code'
|
|
||||||
elif function.startswith('block_'):
|
|
||||||
location = 'block "%s"' % function[6:]
|
|
||||||
else:
|
|
||||||
location = 'template'
|
|
||||||
code = CodeType(0, code.co_nlocals, code.co_stacksize,
|
|
||||||
code.co_flags, code.co_code, code.co_consts,
|
|
||||||
code.co_names, code.co_varnames, filename,
|
|
||||||
location, code.co_firstlineno,
|
|
||||||
code.co_lnotab, (), ())
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# execute the code and catch the new traceback
|
|
||||||
try:
|
|
||||||
exec code in globals, locals
|
|
||||||
except:
|
|
||||||
exc_info = sys.exc_info()
|
|
||||||
new_tb = exc_info[2].tb_next
|
|
||||||
|
|
||||||
# return without this frame
|
|
||||||
return exc_info[:2] + (new_tb,)
|
|
||||||
|
|
||||||
|
|
||||||
def _init_ugly_crap():
|
|
||||||
"""This function implements a few ugly things so that we can patch the
|
|
||||||
traceback objects. The function returned allows resetting `tb_next` on
|
|
||||||
any python traceback object.
|
|
||||||
"""
|
|
||||||
import ctypes
|
|
||||||
from types import TracebackType
|
|
||||||
|
|
||||||
# figure out side of _Py_ssize_t
|
|
||||||
if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
|
|
||||||
_Py_ssize_t = ctypes.c_int64
|
|
||||||
else:
|
|
||||||
_Py_ssize_t = ctypes.c_int
|
|
||||||
|
|
||||||
# regular python
|
|
||||||
class _PyObject(ctypes.Structure):
|
|
||||||
pass
|
|
||||||
_PyObject._fields_ = [
|
|
||||||
('ob_refcnt', _Py_ssize_t),
|
|
||||||
('ob_type', ctypes.POINTER(_PyObject))
|
|
||||||
]
|
|
||||||
|
|
||||||
# python with trace
|
|
||||||
if object.__basicsize__ != ctypes.sizeof(_PyObject):
|
|
||||||
class _PyObject(ctypes.Structure):
|
|
||||||
pass
|
|
||||||
_PyObject._fields_ = [
|
|
||||||
('_ob_next', ctypes.POINTER(_PyObject)),
|
|
||||||
('_ob_prev', ctypes.POINTER(_PyObject)),
|
|
||||||
('ob_refcnt', _Py_ssize_t),
|
|
||||||
('ob_type', ctypes.POINTER(_PyObject))
|
|
||||||
]
|
|
||||||
|
|
||||||
class _Traceback(_PyObject):
|
|
||||||
pass
|
|
||||||
_Traceback._fields_ = [
|
|
||||||
('tb_next', ctypes.POINTER(_Traceback)),
|
|
||||||
('tb_frame', ctypes.POINTER(_PyObject)),
|
|
||||||
('tb_lasti', ctypes.c_int),
|
|
||||||
('tb_lineno', ctypes.c_int)
|
|
||||||
]
|
|
||||||
|
|
||||||
def tb_set_next(tb, next):
|
|
||||||
"""Set the tb_next attribute of a traceback object."""
|
|
||||||
if not (isinstance(tb, TracebackType) and
|
|
||||||
(next is None or isinstance(next, TracebackType))):
|
|
||||||
raise TypeError('tb_set_next arguments must be traceback objects')
|
|
||||||
obj = _Traceback.from_address(id(tb))
|
|
||||||
if tb.tb_next is not None:
|
|
||||||
old = _Traceback.from_address(id(tb.tb_next))
|
|
||||||
old.ob_refcnt -= 1
|
|
||||||
if next is None:
|
|
||||||
obj.tb_next = ctypes.POINTER(_Traceback)()
|
|
||||||
else:
|
|
||||||
next = _Traceback.from_address(id(next))
|
|
||||||
next.ob_refcnt += 1
|
|
||||||
obj.tb_next = ctypes.pointer(next)
|
|
||||||
|
|
||||||
return tb_set_next
|
|
||||||
|
|
||||||
|
|
||||||
# try to get a tb_set_next implementation
|
|
||||||
try:
|
|
||||||
from jinja2._speedups import tb_set_next
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
tb_set_next = _init_ugly_crap()
|
|
||||||
except:
|
|
||||||
tb_set_next = None
|
|
||||||
del _init_ugly_crap
|
|
|
@ -1,40 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.defaults
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja default filters and tags.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner
|
|
||||||
|
|
||||||
|
|
||||||
# defaults for the parser / lexer
|
|
||||||
BLOCK_START_STRING = '{%'
|
|
||||||
BLOCK_END_STRING = '%}'
|
|
||||||
VARIABLE_START_STRING = '{{'
|
|
||||||
VARIABLE_END_STRING = '}}'
|
|
||||||
COMMENT_START_STRING = '{#'
|
|
||||||
COMMENT_END_STRING = '#}'
|
|
||||||
LINE_STATEMENT_PREFIX = None
|
|
||||||
LINE_COMMENT_PREFIX = None
|
|
||||||
TRIM_BLOCKS = False
|
|
||||||
NEWLINE_SEQUENCE = '\n'
|
|
||||||
|
|
||||||
|
|
||||||
# default filters, tests and namespace
|
|
||||||
from jinja2.filters import FILTERS as DEFAULT_FILTERS
|
|
||||||
from jinja2.tests import TESTS as DEFAULT_TESTS
|
|
||||||
DEFAULT_NAMESPACE = {
|
|
||||||
'range': xrange,
|
|
||||||
'dict': lambda **kw: kw,
|
|
||||||
'lipsum': generate_lorem_ipsum,
|
|
||||||
'cycler': Cycler,
|
|
||||||
'joiner': Joiner
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# export all constants
|
|
||||||
__all__ = tuple(x for x in locals().keys() if x.isupper())
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,143 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.exceptions
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja exceptions.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateError(Exception):
|
|
||||||
"""Baseclass for all template errors."""
|
|
||||||
|
|
||||||
def __init__(self, message=None):
|
|
||||||
if message is not None:
|
|
||||||
message = unicode(message).encode('utf-8')
|
|
||||||
Exception.__init__(self, message)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
if self.args:
|
|
||||||
message = self.args[0]
|
|
||||||
if message is not None:
|
|
||||||
return message.decode('utf-8', 'replace')
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateNotFound(IOError, LookupError, TemplateError):
|
|
||||||
"""Raised if a template does not exist."""
|
|
||||||
|
|
||||||
# looks weird, but removes the warning descriptor that just
|
|
||||||
# bogusly warns us about message being deprecated
|
|
||||||
message = None
|
|
||||||
|
|
||||||
def __init__(self, name, message=None):
|
|
||||||
IOError.__init__(self)
|
|
||||||
if message is None:
|
|
||||||
message = name
|
|
||||||
self.message = message
|
|
||||||
self.name = name
|
|
||||||
self.templates = [name]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.message.encode('utf-8')
|
|
||||||
|
|
||||||
# unicode goes after __str__ because we configured 2to3 to rename
|
|
||||||
# __unicode__ to __str__. because the 2to3 tree is not designed to
|
|
||||||
# remove nodes from it, we leave the above __str__ around and let
|
|
||||||
# it override at runtime.
|
|
||||||
def __unicode__(self):
|
|
||||||
return self.message
|
|
||||||
|
|
||||||
|
|
||||||
class TemplatesNotFound(TemplateNotFound):
|
|
||||||
"""Like :class:`TemplateNotFound` but raised if multiple templates
|
|
||||||
are selected. This is a subclass of :class:`TemplateNotFound`
|
|
||||||
exception, so just catching the base exception will catch both.
|
|
||||||
|
|
||||||
.. versionadded:: 2.2
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, names=(), message=None):
|
|
||||||
if message is None:
|
|
||||||
message = u'non of the templates given were found: ' + \
|
|
||||||
u', '.join(map(unicode, names))
|
|
||||||
TemplateNotFound.__init__(self, names and names[-1] or None, message)
|
|
||||||
self.templates = list(names)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateSyntaxError(TemplateError):
|
|
||||||
"""Raised to tell the user that there is a problem with the template."""
|
|
||||||
|
|
||||||
def __init__(self, message, lineno, name=None, filename=None):
|
|
||||||
TemplateError.__init__(self, message)
|
|
||||||
self.lineno = lineno
|
|
||||||
self.name = name
|
|
||||||
self.filename = filename
|
|
||||||
self.source = None
|
|
||||||
|
|
||||||
# this is set to True if the debug.translate_syntax_error
|
|
||||||
# function translated the syntax error into a new traceback
|
|
||||||
self.translated = False
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return unicode(self).encode('utf-8')
|
|
||||||
|
|
||||||
# unicode goes after __str__ because we configured 2to3 to rename
|
|
||||||
# __unicode__ to __str__. because the 2to3 tree is not designed to
|
|
||||||
# remove nodes from it, we leave the above __str__ around and let
|
|
||||||
# it override at runtime.
|
|
||||||
def __unicode__(self):
|
|
||||||
# for translated errors we only return the message
|
|
||||||
if self.translated:
|
|
||||||
return self.message
|
|
||||||
|
|
||||||
# otherwise attach some stuff
|
|
||||||
location = 'line %d' % self.lineno
|
|
||||||
name = self.filename or self.name
|
|
||||||
if name:
|
|
||||||
location = 'File "%s", %s' % (name, location)
|
|
||||||
lines = [self.message, ' ' + location]
|
|
||||||
|
|
||||||
# if the source is set, add the line to the output
|
|
||||||
if self.source is not None:
|
|
||||||
try:
|
|
||||||
line = self.source.splitlines()[self.lineno - 1]
|
|
||||||
except IndexError:
|
|
||||||
line = None
|
|
||||||
if line:
|
|
||||||
lines.append(' ' + line.strip())
|
|
||||||
|
|
||||||
return u'\n'.join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateAssertionError(TemplateSyntaxError):
|
|
||||||
"""Like a template syntax error, but covers cases where something in the
|
|
||||||
template caused an error at compile time that wasn't necessarily caused
|
|
||||||
by a syntax error. However it's a direct subclass of
|
|
||||||
:exc:`TemplateSyntaxError` and has the same attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateRuntimeError(TemplateError):
|
|
||||||
"""A generic runtime error in the template engine. Under some situations
|
|
||||||
Jinja may raise this exception.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class UndefinedError(TemplateRuntimeError):
|
|
||||||
"""Raised if a template tries to operate on :class:`Undefined`."""
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityError(TemplateRuntimeError):
|
|
||||||
"""Raised if a template tries to do something insecure if the
|
|
||||||
sandbox is enabled.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterArgumentError(TemplateRuntimeError):
|
|
||||||
"""This error is raised if a filter was called with inappropriate
|
|
||||||
arguments
|
|
||||||
"""
|
|
|
@ -1,553 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.ext
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja extensions allow to add custom tags similar to the way django custom
|
|
||||||
tags work. By default two example extensions exist: an i18n and a cache
|
|
||||||
extension.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD.
|
|
||||||
"""
|
|
||||||
from collections import deque
|
|
||||||
from jinja2 import nodes
|
|
||||||
from jinja2.defaults import *
|
|
||||||
from jinja2.environment import get_spontaneous_environment
|
|
||||||
from jinja2.runtime import Undefined, concat
|
|
||||||
from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
|
|
||||||
from jinja2.utils import contextfunction, import_string, Markup, next
|
|
||||||
|
|
||||||
|
|
||||||
# the only real useful gettext functions for a Jinja template. Note
|
|
||||||
# that ugettext must be assigned to gettext as Jinja doesn't support
|
|
||||||
# non unicode strings.
|
|
||||||
GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext')
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionRegistry(type):
|
|
||||||
"""Gives the extension an unique identifier."""
|
|
||||||
|
|
||||||
def __new__(cls, name, bases, d):
|
|
||||||
rv = type.__new__(cls, name, bases, d)
|
|
||||||
rv.identifier = rv.__module__ + '.' + rv.__name__
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class Extension(object):
|
|
||||||
"""Extensions can be used to add extra functionality to the Jinja template
|
|
||||||
system at the parser level. Custom extensions are bound to an environment
|
|
||||||
but may not store environment specific data on `self`. The reason for
|
|
||||||
this is that an extension can be bound to another environment (for
|
|
||||||
overlays) by creating a copy and reassigning the `environment` attribute.
|
|
||||||
|
|
||||||
As extensions are created by the environment they cannot accept any
|
|
||||||
arguments for configuration. One may want to work around that by using
|
|
||||||
a factory function, but that is not possible as extensions are identified
|
|
||||||
by their import name. The correct way to configure the extension is
|
|
||||||
storing the configuration values on the environment. Because this way the
|
|
||||||
environment ends up acting as central configuration storage the
|
|
||||||
attributes may clash which is why extensions have to ensure that the names
|
|
||||||
they choose for configuration are not too generic. ``prefix`` for example
|
|
||||||
is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
|
|
||||||
name as includes the name of the extension (fragment cache).
|
|
||||||
"""
|
|
||||||
__metaclass__ = ExtensionRegistry
|
|
||||||
|
|
||||||
#: if this extension parses this is the list of tags it's listening to.
|
|
||||||
tags = set()
|
|
||||||
|
|
||||||
#: the priority of that extension. This is especially useful for
|
|
||||||
#: extensions that preprocess values. A lower value means higher
|
|
||||||
#: priority.
|
|
||||||
#:
|
|
||||||
#: .. versionadded:: 2.4
|
|
||||||
priority = 100
|
|
||||||
|
|
||||||
def __init__(self, environment):
|
|
||||||
self.environment = environment
|
|
||||||
|
|
||||||
def bind(self, environment):
|
|
||||||
"""Create a copy of this extension bound to another environment."""
|
|
||||||
rv = object.__new__(self.__class__)
|
|
||||||
rv.__dict__.update(self.__dict__)
|
|
||||||
rv.environment = environment
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def preprocess(self, source, name, filename=None):
|
|
||||||
"""This method is called before the actual lexing and can be used to
|
|
||||||
preprocess the source. The `filename` is optional. The return value
|
|
||||||
must be the preprocessed source.
|
|
||||||
"""
|
|
||||||
return source
|
|
||||||
|
|
||||||
def filter_stream(self, stream):
|
|
||||||
"""It's passed a :class:`~jinja2.lexer.TokenStream` that can be used
|
|
||||||
to filter tokens returned. This method has to return an iterable of
|
|
||||||
:class:`~jinja2.lexer.Token`\s, but it doesn't have to return a
|
|
||||||
:class:`~jinja2.lexer.TokenStream`.
|
|
||||||
|
|
||||||
In the `ext` folder of the Jinja2 source distribution there is a file
|
|
||||||
called `inlinegettext.py` which implements a filter that utilizes this
|
|
||||||
method.
|
|
||||||
"""
|
|
||||||
return stream
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
"""If any of the :attr:`tags` matched this method is called with the
|
|
||||||
parser as first argument. The token the parser stream is pointing at
|
|
||||||
is the name token that matched. This method has to return one or a
|
|
||||||
list of multiple nodes.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def attr(self, name, lineno=None):
|
|
||||||
"""Return an attribute node for the current extension. This is useful
|
|
||||||
to pass constants on extensions to generated template code::
|
|
||||||
|
|
||||||
self.attr('_my_attribute', lineno=lineno)
|
|
||||||
"""
|
|
||||||
return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
|
|
||||||
|
|
||||||
def call_method(self, name, args=None, kwargs=None, dyn_args=None,
|
|
||||||
dyn_kwargs=None, lineno=None):
|
|
||||||
"""Call a method of the extension. This is a shortcut for
|
|
||||||
:meth:`attr` + :class:`jinja2.nodes.Call`.
|
|
||||||
"""
|
|
||||||
if args is None:
|
|
||||||
args = []
|
|
||||||
if kwargs is None:
|
|
||||||
kwargs = []
|
|
||||||
return nodes.Call(self.attr(name, lineno=lineno), args, kwargs,
|
|
||||||
dyn_args, dyn_kwargs, lineno=lineno)
|
|
||||||
|
|
||||||
|
|
||||||
@contextfunction
|
|
||||||
def _gettext_alias(context, string):
|
|
||||||
return context.resolve('gettext')(string)
|
|
||||||
|
|
||||||
|
|
||||||
class InternationalizationExtension(Extension):
|
|
||||||
"""This extension adds gettext support to Jinja2."""
|
|
||||||
tags = set(['trans'])
|
|
||||||
|
|
||||||
# TODO: the i18n extension is currently reevaluating values in a few
|
|
||||||
# situations. Take this example:
|
|
||||||
# {% trans count=something() %}{{ count }} foo{% pluralize
|
|
||||||
# %}{{ count }} fooss{% endtrans %}
|
|
||||||
# something is called twice here. One time for the gettext value and
|
|
||||||
# the other time for the n-parameter of the ngettext function.
|
|
||||||
|
|
||||||
def __init__(self, environment):
|
|
||||||
Extension.__init__(self, environment)
|
|
||||||
environment.globals['_'] = _gettext_alias
|
|
||||||
environment.extend(
|
|
||||||
install_gettext_translations=self._install,
|
|
||||||
install_null_translations=self._install_null,
|
|
||||||
uninstall_gettext_translations=self._uninstall,
|
|
||||||
extract_translations=self._extract
|
|
||||||
)
|
|
||||||
|
|
||||||
def _install(self, translations):
|
|
||||||
gettext = getattr(translations, 'ugettext', None)
|
|
||||||
if gettext is None:
|
|
||||||
gettext = translations.gettext
|
|
||||||
ngettext = getattr(translations, 'ungettext', None)
|
|
||||||
if ngettext is None:
|
|
||||||
ngettext = translations.ngettext
|
|
||||||
self.environment.globals.update(gettext=gettext, ngettext=ngettext)
|
|
||||||
|
|
||||||
def _install_null(self):
|
|
||||||
self.environment.globals.update(
|
|
||||||
gettext=lambda x: x,
|
|
||||||
ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _uninstall(self, translations):
|
|
||||||
for key in 'gettext', 'ngettext':
|
|
||||||
self.environment.globals.pop(key, None)
|
|
||||||
|
|
||||||
def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS):
|
|
||||||
if isinstance(source, basestring):
|
|
||||||
source = self.environment.parse(source)
|
|
||||||
return extract_from_ast(source, gettext_functions)
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
"""Parse a translatable tag."""
|
|
||||||
lineno = next(parser.stream).lineno
|
|
||||||
|
|
||||||
# find all the variables referenced. Additionally a variable can be
|
|
||||||
# defined in the body of the trans block too, but this is checked at
|
|
||||||
# a later state.
|
|
||||||
plural_expr = None
|
|
||||||
variables = {}
|
|
||||||
while parser.stream.current.type != 'block_end':
|
|
||||||
if variables:
|
|
||||||
parser.stream.expect('comma')
|
|
||||||
|
|
||||||
# skip colon for python compatibility
|
|
||||||
if parser.stream.skip_if('colon'):
|
|
||||||
break
|
|
||||||
|
|
||||||
name = parser.stream.expect('name')
|
|
||||||
if name.value in variables:
|
|
||||||
parser.fail('translatable variable %r defined twice.' %
|
|
||||||
name.value, name.lineno,
|
|
||||||
exc=TemplateAssertionError)
|
|
||||||
|
|
||||||
# expressions
|
|
||||||
if parser.stream.current.type == 'assign':
|
|
||||||
next(parser.stream)
|
|
||||||
variables[name.value] = var = parser.parse_expression()
|
|
||||||
else:
|
|
||||||
variables[name.value] = var = nodes.Name(name.value, 'load')
|
|
||||||
if plural_expr is None:
|
|
||||||
plural_expr = var
|
|
||||||
|
|
||||||
parser.stream.expect('block_end')
|
|
||||||
|
|
||||||
plural = plural_names = None
|
|
||||||
have_plural = False
|
|
||||||
referenced = set()
|
|
||||||
|
|
||||||
# now parse until endtrans or pluralize
|
|
||||||
singular_names, singular = self._parse_block(parser, True)
|
|
||||||
if singular_names:
|
|
||||||
referenced.update(singular_names)
|
|
||||||
if plural_expr is None:
|
|
||||||
plural_expr = nodes.Name(singular_names[0], 'load')
|
|
||||||
|
|
||||||
# if we have a pluralize block, we parse that too
|
|
||||||
if parser.stream.current.test('name:pluralize'):
|
|
||||||
have_plural = True
|
|
||||||
next(parser.stream)
|
|
||||||
if parser.stream.current.type != 'block_end':
|
|
||||||
name = parser.stream.expect('name')
|
|
||||||
if name.value not in variables:
|
|
||||||
parser.fail('unknown variable %r for pluralization' %
|
|
||||||
name.value, name.lineno,
|
|
||||||
exc=TemplateAssertionError)
|
|
||||||
plural_expr = variables[name.value]
|
|
||||||
parser.stream.expect('block_end')
|
|
||||||
plural_names, plural = self._parse_block(parser, False)
|
|
||||||
next(parser.stream)
|
|
||||||
referenced.update(plural_names)
|
|
||||||
else:
|
|
||||||
next(parser.stream)
|
|
||||||
|
|
||||||
# register free names as simple name expressions
|
|
||||||
for var in referenced:
|
|
||||||
if var not in variables:
|
|
||||||
variables[var] = nodes.Name(var, 'load')
|
|
||||||
|
|
||||||
# no variables referenced? no need to escape
|
|
||||||
if not referenced:
|
|
||||||
singular = singular.replace('%%', '%')
|
|
||||||
if plural:
|
|
||||||
plural = plural.replace('%%', '%')
|
|
||||||
|
|
||||||
if not have_plural:
|
|
||||||
plural_expr = None
|
|
||||||
elif plural_expr is None:
|
|
||||||
parser.fail('pluralize without variables', lineno)
|
|
||||||
|
|
||||||
if variables:
|
|
||||||
variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
|
|
||||||
for x, y in variables.items()])
|
|
||||||
else:
|
|
||||||
variables = None
|
|
||||||
|
|
||||||
node = self._make_node(singular, plural, variables, plural_expr)
|
|
||||||
node.set_lineno(lineno)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _parse_block(self, parser, allow_pluralize):
|
|
||||||
"""Parse until the next block tag with a given name."""
|
|
||||||
referenced = []
|
|
||||||
buf = []
|
|
||||||
while 1:
|
|
||||||
if parser.stream.current.type == 'data':
|
|
||||||
buf.append(parser.stream.current.value.replace('%', '%%'))
|
|
||||||
next(parser.stream)
|
|
||||||
elif parser.stream.current.type == 'variable_begin':
|
|
||||||
next(parser.stream)
|
|
||||||
name = parser.stream.expect('name').value
|
|
||||||
referenced.append(name)
|
|
||||||
buf.append('%%(%s)s' % name)
|
|
||||||
parser.stream.expect('variable_end')
|
|
||||||
elif parser.stream.current.type == 'block_begin':
|
|
||||||
next(parser.stream)
|
|
||||||
if parser.stream.current.test('name:endtrans'):
|
|
||||||
break
|
|
||||||
elif parser.stream.current.test('name:pluralize'):
|
|
||||||
if allow_pluralize:
|
|
||||||
break
|
|
||||||
parser.fail('a translatable section can have only one '
|
|
||||||
'pluralize section')
|
|
||||||
parser.fail('control structures in translatable sections are '
|
|
||||||
'not allowed')
|
|
||||||
elif parser.stream.eos:
|
|
||||||
parser.fail('unclosed translation block')
|
|
||||||
else:
|
|
||||||
assert False, 'internal parser error'
|
|
||||||
|
|
||||||
return referenced, concat(buf)
|
|
||||||
|
|
||||||
def _make_node(self, singular, plural, variables, plural_expr):
|
|
||||||
"""Generates a useful node from the data provided."""
|
|
||||||
# singular only:
|
|
||||||
if plural_expr is None:
|
|
||||||
gettext = nodes.Name('gettext', 'load')
|
|
||||||
node = nodes.Call(gettext, [nodes.Const(singular)],
|
|
||||||
[], None, None)
|
|
||||||
|
|
||||||
# singular and plural
|
|
||||||
else:
|
|
||||||
ngettext = nodes.Name('ngettext', 'load')
|
|
||||||
node = nodes.Call(ngettext, [
|
|
||||||
nodes.Const(singular),
|
|
||||||
nodes.Const(plural),
|
|
||||||
plural_expr
|
|
||||||
], [], None, None)
|
|
||||||
|
|
||||||
# mark the return value as safe if we are in an
|
|
||||||
# environment with autoescaping turned on
|
|
||||||
if self.environment.autoescape:
|
|
||||||
node = nodes.MarkSafe(node)
|
|
||||||
|
|
||||||
if variables:
|
|
||||||
node = nodes.Mod(node, variables)
|
|
||||||
return nodes.Output([node])
|
|
||||||
|
|
||||||
|
|
||||||
class ExprStmtExtension(Extension):
|
|
||||||
"""Adds a `do` tag to Jinja2 that works like the print statement just
|
|
||||||
that it doesn't print the return value.
|
|
||||||
"""
|
|
||||||
tags = set(['do'])
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
node = nodes.ExprStmt(lineno=next(parser.stream).lineno)
|
|
||||||
node.node = parser.parse_tuple()
|
|
||||||
return node
|
|
||||||
|
|
||||||
|
|
||||||
class LoopControlExtension(Extension):
|
|
||||||
"""Adds break and continue to the template engine."""
|
|
||||||
tags = set(['break', 'continue'])
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
token = next(parser.stream)
|
|
||||||
if token.value == 'break':
|
|
||||||
return nodes.Break(lineno=token.lineno)
|
|
||||||
return nodes.Continue(lineno=token.lineno)
|
|
||||||
|
|
||||||
|
|
||||||
class WithExtension(Extension):
|
|
||||||
"""Adds support for a django-like with block."""
|
|
||||||
tags = set(['with'])
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
node = nodes.Scope(lineno=next(parser.stream).lineno)
|
|
||||||
assignments = []
|
|
||||||
while parser.stream.current.type != 'block_end':
|
|
||||||
lineno = parser.stream.current.lineno
|
|
||||||
if assignments:
|
|
||||||
parser.stream.expect('comma')
|
|
||||||
target = parser.parse_assign_target()
|
|
||||||
parser.stream.expect('assign')
|
|
||||||
expr = parser.parse_expression()
|
|
||||||
assignments.append(nodes.Assign(target, expr, lineno=lineno))
|
|
||||||
node.body = assignments + \
|
|
||||||
list(parser.parse_statements(('name:endwith',),
|
|
||||||
drop_needle=True))
|
|
||||||
return node
|
|
||||||
|
|
||||||
|
|
||||||
class AutoEscapeExtension(Extension):
|
|
||||||
"""Changes auto escape rules for a scope."""
|
|
||||||
tags = set(['autoescape'])
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
node = nodes.ScopedEvalContextModifier(lineno=next(parser.stream).lineno)
|
|
||||||
node.options = [
|
|
||||||
nodes.Keyword('autoescape', parser.parse_expression())
|
|
||||||
]
|
|
||||||
node.body = parser.parse_statements(('name:endautoescape',),
|
|
||||||
drop_needle=True)
|
|
||||||
return nodes.Scope([node])
|
|
||||||
|
|
||||||
|
|
||||||
def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS,
|
|
||||||
babel_style=True):
|
|
||||||
"""Extract localizable strings from the given template node. Per
|
|
||||||
default this function returns matches in babel style that means non string
|
|
||||||
parameters as well as keyword arguments are returned as `None`. This
|
|
||||||
allows Babel to figure out what you really meant if you are using
|
|
||||||
gettext functions that allow keyword arguments for placeholder expansion.
|
|
||||||
If you don't want that behavior set the `babel_style` parameter to `False`
|
|
||||||
which causes only strings to be returned and parameters are always stored
|
|
||||||
in tuples. As a consequence invalid gettext calls (calls without a single
|
|
||||||
string parameter or string parameters after non-string parameters) are
|
|
||||||
skipped.
|
|
||||||
|
|
||||||
This example explains the behavior:
|
|
||||||
|
|
||||||
>>> from jinja2 import Environment
|
|
||||||
>>> env = Environment()
|
|
||||||
>>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}')
|
|
||||||
>>> list(extract_from_ast(node))
|
|
||||||
[(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))]
|
|
||||||
>>> list(extract_from_ast(node, babel_style=False))
|
|
||||||
[(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))]
|
|
||||||
|
|
||||||
For every string found this function yields a ``(lineno, function,
|
|
||||||
message)`` tuple, where:
|
|
||||||
|
|
||||||
* ``lineno`` is the number of the line on which the string was found,
|
|
||||||
* ``function`` is the name of the ``gettext`` function used (if the
|
|
||||||
string was extracted from embedded Python code), and
|
|
||||||
* ``message`` is the string itself (a ``unicode`` object, or a tuple
|
|
||||||
of ``unicode`` objects for functions with multiple string arguments).
|
|
||||||
|
|
||||||
This extraction function operates on the AST and is because of that unable
|
|
||||||
to extract any comments. For comment support you have to use the babel
|
|
||||||
extraction interface or extract comments yourself.
|
|
||||||
"""
|
|
||||||
for node in node.find_all(nodes.Call):
|
|
||||||
if not isinstance(node.node, nodes.Name) or \
|
|
||||||
node.node.name not in gettext_functions:
|
|
||||||
continue
|
|
||||||
|
|
||||||
strings = []
|
|
||||||
for arg in node.args:
|
|
||||||
if isinstance(arg, nodes.Const) and \
|
|
||||||
isinstance(arg.value, basestring):
|
|
||||||
strings.append(arg.value)
|
|
||||||
else:
|
|
||||||
strings.append(None)
|
|
||||||
|
|
||||||
for arg in node.kwargs:
|
|
||||||
strings.append(None)
|
|
||||||
if node.dyn_args is not None:
|
|
||||||
strings.append(None)
|
|
||||||
if node.dyn_kwargs is not None:
|
|
||||||
strings.append(None)
|
|
||||||
|
|
||||||
if not babel_style:
|
|
||||||
strings = tuple(x for x in strings if x is not None)
|
|
||||||
if not strings:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if len(strings) == 1:
|
|
||||||
strings = strings[0]
|
|
||||||
else:
|
|
||||||
strings = tuple(strings)
|
|
||||||
yield node.lineno, node.node.name, strings
|
|
||||||
|
|
||||||
|
|
||||||
class _CommentFinder(object):
|
|
||||||
"""Helper class to find comments in a token stream. Can only
|
|
||||||
find comments for gettext calls forwards. Once the comment
|
|
||||||
from line 4 is found, a comment for line 1 will not return a
|
|
||||||
usable value.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tokens, comment_tags):
|
|
||||||
self.tokens = tokens
|
|
||||||
self.comment_tags = comment_tags
|
|
||||||
self.offset = 0
|
|
||||||
self.last_lineno = 0
|
|
||||||
|
|
||||||
def find_backwards(self, offset):
|
|
||||||
try:
|
|
||||||
for _, token_type, token_value in \
|
|
||||||
reversed(self.tokens[self.offset:offset]):
|
|
||||||
if token_type in ('comment', 'linecomment'):
|
|
||||||
try:
|
|
||||||
prefix, comment = token_value.split(None, 1)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if prefix in self.comment_tags:
|
|
||||||
return [comment.rstrip()]
|
|
||||||
return []
|
|
||||||
finally:
|
|
||||||
self.offset = offset
|
|
||||||
|
|
||||||
def find_comments(self, lineno):
|
|
||||||
if not self.comment_tags or self.last_lineno > lineno:
|
|
||||||
return []
|
|
||||||
for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset:]):
|
|
||||||
if token_lineno > lineno:
|
|
||||||
return self.find_backwards(self.offset + idx)
|
|
||||||
return self.find_backwards(len(self.tokens))
|
|
||||||
|
|
||||||
|
|
||||||
def babel_extract(fileobj, keywords, comment_tags, options):
|
|
||||||
"""Babel extraction method for Jinja templates.
|
|
||||||
|
|
||||||
.. versionchanged:: 2.3
|
|
||||||
Basic support for translation comments was added. If `comment_tags`
|
|
||||||
is now set to a list of keywords for extraction, the extractor will
|
|
||||||
try to find the best preceeding comment that begins with one of the
|
|
||||||
keywords. For best results, make sure to not have more than one
|
|
||||||
gettext call in one line of code and the matching comment in the
|
|
||||||
same line or the line before.
|
|
||||||
|
|
||||||
:param fileobj: the file-like object the messages should be extracted from
|
|
||||||
:param keywords: a list of keywords (i.e. function names) that should be
|
|
||||||
recognized as translation functions
|
|
||||||
:param comment_tags: a list of translator tags to search for and include
|
|
||||||
in the results.
|
|
||||||
:param options: a dictionary of additional options (optional)
|
|
||||||
:return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
|
|
||||||
(comments will be empty currently)
|
|
||||||
"""
|
|
||||||
extensions = set()
|
|
||||||
for extension in options.get('extensions', '').split(','):
|
|
||||||
extension = extension.strip()
|
|
||||||
if not extension:
|
|
||||||
continue
|
|
||||||
extensions.add(import_string(extension))
|
|
||||||
if InternationalizationExtension not in extensions:
|
|
||||||
extensions.add(InternationalizationExtension)
|
|
||||||
|
|
||||||
environment = get_spontaneous_environment(
|
|
||||||
options.get('block_start_string', BLOCK_START_STRING),
|
|
||||||
options.get('block_end_string', BLOCK_END_STRING),
|
|
||||||
options.get('variable_start_string', VARIABLE_START_STRING),
|
|
||||||
options.get('variable_end_string', VARIABLE_END_STRING),
|
|
||||||
options.get('comment_start_string', COMMENT_START_STRING),
|
|
||||||
options.get('comment_end_string', COMMENT_END_STRING),
|
|
||||||
options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX,
|
|
||||||
options.get('line_comment_prefix') or LINE_COMMENT_PREFIX,
|
|
||||||
str(options.get('trim_blocks', TRIM_BLOCKS)).lower() in \
|
|
||||||
('1', 'on', 'yes', 'true'),
|
|
||||||
NEWLINE_SEQUENCE, frozenset(extensions),
|
|
||||||
# fill with defaults so that environments are shared
|
|
||||||
# with other spontaneus environments. The rest of the
|
|
||||||
# arguments are optimizer, undefined, finalize, autoescape,
|
|
||||||
# loader, cache size, auto reloading setting and the
|
|
||||||
# bytecode cache
|
|
||||||
True, Undefined, None, False, None, 0, False, None
|
|
||||||
)
|
|
||||||
|
|
||||||
source = fileobj.read().decode(options.get('encoding', 'utf-8'))
|
|
||||||
try:
|
|
||||||
node = environment.parse(source)
|
|
||||||
tokens = list(environment.lex(environment.preprocess(source)))
|
|
||||||
except TemplateSyntaxError as e:
|
|
||||||
# skip templates with syntax errors
|
|
||||||
return
|
|
||||||
|
|
||||||
finder = _CommentFinder(tokens, comment_tags)
|
|
||||||
for lineno, func, message in extract_from_ast(node, keywords):
|
|
||||||
yield lineno, func, message, finder.find_comments(lineno)
|
|
||||||
|
|
||||||
|
|
||||||
#: nicer import names
|
|
||||||
i18n = InternationalizationExtension
|
|
||||||
do = ExprStmtExtension
|
|
||||||
loopcontrols = LoopControlExtension
|
|
||||||
with_ = WithExtension
|
|
||||||
autoescape = AutoEscapeExtension
|
|
|
@ -1,731 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.filters
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Bundled jinja filters.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
import math
|
|
||||||
from random import choice
|
|
||||||
from operator import itemgetter
|
|
||||||
from itertools import imap, groupby
|
|
||||||
from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode
|
|
||||||
from jinja2.runtime import Undefined
|
|
||||||
from jinja2.exceptions import FilterArgumentError, SecurityError
|
|
||||||
|
|
||||||
|
|
||||||
_word_re = re.compile(r'\w+(?u)')
|
|
||||||
|
|
||||||
|
|
||||||
def contextfilter(f):
|
|
||||||
"""Decorator for marking context dependent filters. The current
|
|
||||||
:class:`Context` will be passed as first argument.
|
|
||||||
"""
|
|
||||||
f.contextfilter = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def evalcontextfilter(f):
|
|
||||||
"""Decorator for marking eval-context dependent filters. An eval
|
|
||||||
context object is passed as first argument. For more information
|
|
||||||
about the eval context, see :ref:`eval-context`.
|
|
||||||
|
|
||||||
.. versionadded:: 2.4
|
|
||||||
"""
|
|
||||||
f.evalcontextfilter = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def environmentfilter(f):
|
|
||||||
"""Decorator for marking evironment dependent filters. The current
|
|
||||||
:class:`Environment` is passed to the filter as first argument.
|
|
||||||
"""
|
|
||||||
f.environmentfilter = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def do_forceescape(value):
|
|
||||||
"""Enforce HTML escaping. This will probably double escape variables."""
|
|
||||||
if hasattr(value, '__html__'):
|
|
||||||
value = value.__html__()
|
|
||||||
return escape(unicode(value))
|
|
||||||
|
|
||||||
|
|
||||||
@evalcontextfilter
|
|
||||||
def do_replace(eval_ctx, s, old, new, count=None):
|
|
||||||
"""Return a copy of the value with all occurrences of a substring
|
|
||||||
replaced with a new one. The first argument is the substring
|
|
||||||
that should be replaced, the second is the replacement string.
|
|
||||||
If the optional third argument ``count`` is given, only the first
|
|
||||||
``count`` occurrences are replaced:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ "Hello World"|replace("Hello", "Goodbye") }}
|
|
||||||
-> Goodbye World
|
|
||||||
|
|
||||||
{{ "aaaaargh"|replace("a", "d'oh, ", 2) }}
|
|
||||||
-> d'oh, d'oh, aaargh
|
|
||||||
"""
|
|
||||||
if count is None:
|
|
||||||
count = -1
|
|
||||||
if not eval_ctx.autoescape:
|
|
||||||
return unicode(s).replace(unicode(old), unicode(new), count)
|
|
||||||
if hasattr(old, '__html__') or hasattr(new, '__html__') and \
|
|
||||||
not hasattr(s, '__html__'):
|
|
||||||
s = escape(s)
|
|
||||||
else:
|
|
||||||
s = soft_unicode(s)
|
|
||||||
return s.replace(soft_unicode(old), soft_unicode(new), count)
|
|
||||||
|
|
||||||
|
|
||||||
def do_upper(s):
|
|
||||||
"""Convert a value to uppercase."""
|
|
||||||
return soft_unicode(s).upper()
|
|
||||||
|
|
||||||
|
|
||||||
def do_lower(s):
|
|
||||||
"""Convert a value to lowercase."""
|
|
||||||
return soft_unicode(s).lower()
|
|
||||||
|
|
||||||
|
|
||||||
@evalcontextfilter
|
|
||||||
def do_xmlattr(_eval_ctx, d, autospace=True):
|
|
||||||
"""Create an SGML/XML attribute string based on the items in a dict.
|
|
||||||
All values that are neither `none` nor `undefined` are automatically
|
|
||||||
escaped:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<ul{{ {'class': 'my_list', 'missing': none,
|
|
||||||
'id': 'list-%d'|format(variable)}|xmlattr }}>
|
|
||||||
...
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
Results in something like this:
|
|
||||||
|
|
||||||
.. sourcecode:: html
|
|
||||||
|
|
||||||
<ul class="my_list" id="list-42">
|
|
||||||
...
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
As you can see it automatically prepends a space in front of the item
|
|
||||||
if the filter returned something unless the second parameter is false.
|
|
||||||
"""
|
|
||||||
rv = u' '.join(
|
|
||||||
u'%s="%s"' % (escape(key), escape(value))
|
|
||||||
for key, value in d.iteritems()
|
|
||||||
if value is not None and not isinstance(value, Undefined)
|
|
||||||
)
|
|
||||||
if autospace and rv:
|
|
||||||
rv = u' ' + rv
|
|
||||||
if _eval_ctx.autoescape:
|
|
||||||
rv = Markup(rv)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def do_capitalize(s):
|
|
||||||
"""Capitalize a value. The first character will be uppercase, all others
|
|
||||||
lowercase.
|
|
||||||
"""
|
|
||||||
return soft_unicode(s).capitalize()
|
|
||||||
|
|
||||||
|
|
||||||
def do_title(s):
|
|
||||||
"""Return a titlecased version of the value. I.e. words will start with
|
|
||||||
uppercase letters, all remaining characters are lowercase.
|
|
||||||
"""
|
|
||||||
return soft_unicode(s).title()
|
|
||||||
|
|
||||||
|
|
||||||
def do_dictsort(value, case_sensitive=False, by='key'):
|
|
||||||
"""Sort a dict and yield (key, value) pairs. Because python dicts are
|
|
||||||
unsorted you may want to use this function to order them by either
|
|
||||||
key or value:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% for item in mydict|dictsort %}
|
|
||||||
sort the dict by key, case insensitive
|
|
||||||
|
|
||||||
{% for item in mydict|dicsort(true) %}
|
|
||||||
sort the dict by key, case sensitive
|
|
||||||
|
|
||||||
{% for item in mydict|dictsort(false, 'value') %}
|
|
||||||
sort the dict by key, case insensitive, sorted
|
|
||||||
normally and ordered by value.
|
|
||||||
"""
|
|
||||||
if by == 'key':
|
|
||||||
pos = 0
|
|
||||||
elif by == 'value':
|
|
||||||
pos = 1
|
|
||||||
else:
|
|
||||||
raise FilterArgumentError('You can only sort by either '
|
|
||||||
'"key" or "value"')
|
|
||||||
def sort_func(item):
|
|
||||||
value = item[pos]
|
|
||||||
if isinstance(value, basestring) and not case_sensitive:
|
|
||||||
value = value.lower()
|
|
||||||
return value
|
|
||||||
|
|
||||||
return sorted(value.items(), key=sort_func)
|
|
||||||
|
|
||||||
|
|
||||||
def do_sort(value, case_sensitive=False):
|
|
||||||
"""Sort an iterable. If the iterable is made of strings the second
|
|
||||||
parameter can be used to control the case sensitiveness of the
|
|
||||||
comparison which is disabled by default.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% for item in iterable|sort %}
|
|
||||||
...
|
|
||||||
{% endfor %}
|
|
||||||
"""
|
|
||||||
if not case_sensitive:
|
|
||||||
def sort_func(item):
|
|
||||||
if isinstance(item, basestring):
|
|
||||||
item = item.lower()
|
|
||||||
return item
|
|
||||||
else:
|
|
||||||
sort_func = None
|
|
||||||
return sorted(seq, key=sort_func)
|
|
||||||
|
|
||||||
|
|
||||||
def do_default(value, default_value=u'', boolean=False):
|
|
||||||
"""If the value is undefined it will return the passed default value,
|
|
||||||
otherwise the value of the variable:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ my_variable|default('my_variable is not defined') }}
|
|
||||||
|
|
||||||
This will output the value of ``my_variable`` if the variable was
|
|
||||||
defined, otherwise ``'my_variable is not defined'``. If you want
|
|
||||||
to use default with variables that evaluate to false you have to
|
|
||||||
set the second parameter to `true`:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ ''|default('the string was empty', true) }}
|
|
||||||
"""
|
|
||||||
if (boolean and not value) or isinstance(value, Undefined):
|
|
||||||
return default_value
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@evalcontextfilter
|
|
||||||
def do_join(eval_ctx, value, d=u''):
|
|
||||||
"""Return a string which is the concatenation of the strings in the
|
|
||||||
sequence. The separator between elements is an empty string per
|
|
||||||
default, you can define it with the optional parameter:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ [1, 2, 3]|join('|') }}
|
|
||||||
-> 1|2|3
|
|
||||||
|
|
||||||
{{ [1, 2, 3]|join }}
|
|
||||||
-> 123
|
|
||||||
"""
|
|
||||||
# no automatic escaping? joining is a lot eaiser then
|
|
||||||
if not eval_ctx.autoescape:
|
|
||||||
return unicode(d).join(imap(unicode, value))
|
|
||||||
|
|
||||||
# if the delimiter doesn't have an html representation we check
|
|
||||||
# if any of the items has. If yes we do a coercion to Markup
|
|
||||||
if not hasattr(d, '__html__'):
|
|
||||||
value = list(value)
|
|
||||||
do_escape = False
|
|
||||||
for idx, item in enumerate(value):
|
|
||||||
if hasattr(item, '__html__'):
|
|
||||||
do_escape = True
|
|
||||||
else:
|
|
||||||
value[idx] = unicode(item)
|
|
||||||
if do_escape:
|
|
||||||
d = escape(d)
|
|
||||||
else:
|
|
||||||
d = unicode(d)
|
|
||||||
return d.join(value)
|
|
||||||
|
|
||||||
# no html involved, to normal joining
|
|
||||||
return soft_unicode(d).join(imap(soft_unicode, value))
|
|
||||||
|
|
||||||
|
|
||||||
def do_center(value, width=80):
|
|
||||||
"""Centers the value in a field of a given width."""
|
|
||||||
return unicode(value).center(width)
|
|
||||||
|
|
||||||
|
|
||||||
@environmentfilter
|
|
||||||
def do_first(environment, seq):
|
|
||||||
"""Return the first item of a sequence."""
|
|
||||||
try:
|
|
||||||
return iter(seq).next()
|
|
||||||
except StopIteration:
|
|
||||||
return environment.undefined('No first item, sequence was empty.')
|
|
||||||
|
|
||||||
|
|
||||||
@environmentfilter
|
|
||||||
def do_last(environment, seq):
|
|
||||||
"""Return the last item of a sequence."""
|
|
||||||
try:
|
|
||||||
return iter(reversed(seq)).next()
|
|
||||||
except StopIteration:
|
|
||||||
return environment.undefined('No last item, sequence was empty.')
|
|
||||||
|
|
||||||
|
|
||||||
@environmentfilter
|
|
||||||
def do_random(environment, seq):
|
|
||||||
"""Return a random item from the sequence."""
|
|
||||||
try:
|
|
||||||
return choice(seq)
|
|
||||||
except IndexError:
|
|
||||||
return environment.undefined('No random item, sequence was empty.')
|
|
||||||
|
|
||||||
|
|
||||||
def do_filesizeformat(value, binary=False):
|
|
||||||
"""Format the value like a 'human-readable' file size (i.e. 13 KB,
|
|
||||||
4.1 MB, 102 bytes, etc). Per default decimal prefixes are used (mega,
|
|
||||||
giga, etc.), if the second parameter is set to `True` the binary
|
|
||||||
prefixes are used (mebi, gibi).
|
|
||||||
"""
|
|
||||||
bytes = float(value)
|
|
||||||
base = binary and 1024 or 1000
|
|
||||||
middle = binary and 'i' or ''
|
|
||||||
if bytes < base:
|
|
||||||
return "%d Byte%s" % (bytes, bytes != 1 and 's' or '')
|
|
||||||
elif bytes < base * base:
|
|
||||||
return "%.1f K%sB" % (bytes / base, middle)
|
|
||||||
elif bytes < base * base * base:
|
|
||||||
return "%.1f M%sB" % (bytes / (base * base), middle)
|
|
||||||
return "%.1f G%sB" % (bytes / (base * base * base), middle)
|
|
||||||
|
|
||||||
|
|
||||||
def do_pprint(value, verbose=False):
|
|
||||||
"""Pretty print a variable. Useful for debugging.
|
|
||||||
|
|
||||||
With Jinja 1.2 onwards you can pass it a parameter. If this parameter
|
|
||||||
is truthy the output will be more verbose (this requires `pretty`)
|
|
||||||
"""
|
|
||||||
return pformat(value, verbose=verbose)
|
|
||||||
|
|
||||||
|
|
||||||
@evalcontextfilter
|
|
||||||
def do_urlize(eval_ctx, value, trim_url_limit=None, nofollow=False):
|
|
||||||
"""Converts URLs in plain text into clickable links.
|
|
||||||
|
|
||||||
If you pass the filter an additional integer it will shorten the urls
|
|
||||||
to that number. Also a third argument exists that makes the urls
|
|
||||||
"nofollow":
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ mytext|urlize(40, true) }}
|
|
||||||
links are shortened to 40 chars and defined with rel="nofollow"
|
|
||||||
"""
|
|
||||||
rv = urlize(value, trim_url_limit, nofollow)
|
|
||||||
if eval_ctx.autoescape:
|
|
||||||
rv = Markup(rv)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def do_indent(s, width=4, indentfirst=False):
|
|
||||||
"""Return a copy of the passed string, each line indented by
|
|
||||||
4 spaces. The first line is not indented. If you want to
|
|
||||||
change the number of spaces or indent the first line too
|
|
||||||
you can pass additional parameters to the filter:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ mytext|indent(2, true) }}
|
|
||||||
indent by two spaces and indent the first line too.
|
|
||||||
"""
|
|
||||||
indention = u' ' * width
|
|
||||||
rv = (u'\n' + indention).join(s.splitlines())
|
|
||||||
if indentfirst:
|
|
||||||
rv = indention + rv
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def do_truncate(s, length=255, killwords=False, end='...'):
|
|
||||||
"""Return a truncated copy of the string. The length is specified
|
|
||||||
with the first parameter which defaults to ``255``. If the second
|
|
||||||
parameter is ``true`` the filter will cut the text at length. Otherwise
|
|
||||||
it will try to save the last word. If the text was in fact
|
|
||||||
truncated it will append an ellipsis sign (``"..."``). If you want a
|
|
||||||
different ellipsis sign than ``"..."`` you can specify it using the
|
|
||||||
third parameter.
|
|
||||||
|
|
||||||
.. sourcecode jinja::
|
|
||||||
|
|
||||||
{{ mytext|truncate(300, false, '»') }}
|
|
||||||
truncate mytext to 300 chars, don't split up words, use a
|
|
||||||
right pointing double arrow as ellipsis sign.
|
|
||||||
"""
|
|
||||||
if len(s) <= length:
|
|
||||||
return s
|
|
||||||
elif killwords:
|
|
||||||
return s[:length] + end
|
|
||||||
words = s.split(' ')
|
|
||||||
result = []
|
|
||||||
m = 0
|
|
||||||
for word in words:
|
|
||||||
m += len(word) + 1
|
|
||||||
if m > length:
|
|
||||||
break
|
|
||||||
result.append(word)
|
|
||||||
result.append(end)
|
|
||||||
return u' '.join(result)
|
|
||||||
|
|
||||||
|
|
||||||
def do_wordwrap(s, width=79, break_long_words=True):
|
|
||||||
"""
|
|
||||||
Return a copy of the string passed to the filter wrapped after
|
|
||||||
``79`` characters. You can override this default using the first
|
|
||||||
parameter. If you set the second parameter to `false` Jinja will not
|
|
||||||
split words apart if they are longer than `width`.
|
|
||||||
"""
|
|
||||||
import textwrap
|
|
||||||
return u'\n'.join(textwrap.wrap(s, width=width, expand_tabs=False,
|
|
||||||
replace_whitespace=False,
|
|
||||||
break_long_words=break_long_words))
|
|
||||||
|
|
||||||
|
|
||||||
def do_wordcount(s):
|
|
||||||
"""Count the words in that string."""
|
|
||||||
return len(_word_re.findall(s))
|
|
||||||
|
|
||||||
|
|
||||||
def do_int(value, default=0):
|
|
||||||
"""Convert the value into an integer. If the
|
|
||||||
conversion doesn't work it will return ``0``. You can
|
|
||||||
override this default using the first parameter.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return int(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
# this quirk is necessary so that "42.23"|int gives 42.
|
|
||||||
try:
|
|
||||||
return int(float(value))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def do_float(value, default=0.0):
|
|
||||||
"""Convert the value into a floating point number. If the
|
|
||||||
conversion doesn't work it will return ``0.0``. You can
|
|
||||||
override this default using the first parameter.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def do_format(value, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Apply python string formatting on an object:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ "%s - %s"|format("Hello?", "Foo!") }}
|
|
||||||
-> Hello? - Foo!
|
|
||||||
"""
|
|
||||||
if args and kwargs:
|
|
||||||
raise FilterArgumentError('can\'t handle positional and keyword '
|
|
||||||
'arguments at the same time')
|
|
||||||
return soft_unicode(value) % (kwargs or args)
|
|
||||||
|
|
||||||
|
|
||||||
def do_trim(value):
|
|
||||||
"""Strip leading and trailing whitespace."""
|
|
||||||
return soft_unicode(value).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def do_striptags(value):
|
|
||||||
"""Strip SGML/XML tags and replace adjacent whitespace by one space.
|
|
||||||
"""
|
|
||||||
if hasattr(value, '__html__'):
|
|
||||||
value = value.__html__()
|
|
||||||
return Markup(unicode(value)).striptags()
|
|
||||||
|
|
||||||
|
|
||||||
def do_slice(value, slices, fill_with=None):
|
|
||||||
"""Slice an iterator and return a list of lists containing
|
|
||||||
those items. Useful if you want to create a div containing
|
|
||||||
three ul tags that represent columns:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<div class="columwrapper">
|
|
||||||
{%- for column in items|slice(3) %}
|
|
||||||
<ul class="column-{{ loop.index }}">
|
|
||||||
{%- for item in column %}
|
|
||||||
<li>{{ item }}</li>
|
|
||||||
{%- endfor %}
|
|
||||||
</ul>
|
|
||||||
{%- endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
If you pass it a second argument it's used to fill missing
|
|
||||||
values on the last iteration.
|
|
||||||
"""
|
|
||||||
seq = list(value)
|
|
||||||
length = len(seq)
|
|
||||||
items_per_slice = length // slices
|
|
||||||
slices_with_extra = length % slices
|
|
||||||
offset = 0
|
|
||||||
for slice_number in xrange(slices):
|
|
||||||
start = offset + slice_number * items_per_slice
|
|
||||||
if slice_number < slices_with_extra:
|
|
||||||
offset += 1
|
|
||||||
end = offset + (slice_number + 1) * items_per_slice
|
|
||||||
tmp = seq[start:end]
|
|
||||||
if fill_with is not None and slice_number >= slices_with_extra:
|
|
||||||
tmp.append(fill_with)
|
|
||||||
yield tmp
|
|
||||||
|
|
||||||
|
|
||||||
def do_batch(value, linecount, fill_with=None):
|
|
||||||
"""
|
|
||||||
A filter that batches items. It works pretty much like `slice`
|
|
||||||
just the other way round. It returns a list of lists with the
|
|
||||||
given number of items. If you provide a second parameter this
|
|
||||||
is used to fill missing items. See this example:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<table>
|
|
||||||
{%- for row in items|batch(3, ' ') %}
|
|
||||||
<tr>
|
|
||||||
{%- for column in row %}
|
|
||||||
<td>{{ column }}</td>
|
|
||||||
{%- endfor %}
|
|
||||||
</tr>
|
|
||||||
{%- endfor %}
|
|
||||||
</table>
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
tmp = []
|
|
||||||
for item in value:
|
|
||||||
if len(tmp) == linecount:
|
|
||||||
yield tmp
|
|
||||||
tmp = []
|
|
||||||
tmp.append(item)
|
|
||||||
if tmp:
|
|
||||||
if fill_with is not None and len(tmp) < linecount:
|
|
||||||
tmp += [fill_with] * (linecount - len(tmp))
|
|
||||||
yield tmp
|
|
||||||
|
|
||||||
|
|
||||||
def do_round(value, precision=0, method='common'):
|
|
||||||
"""Round the number to a given precision. The first
|
|
||||||
parameter specifies the precision (default is ``0``), the
|
|
||||||
second the rounding method:
|
|
||||||
|
|
||||||
- ``'common'`` rounds either up or down
|
|
||||||
- ``'ceil'`` always rounds up
|
|
||||||
- ``'floor'`` always rounds down
|
|
||||||
|
|
||||||
If you don't specify a method ``'common'`` is used.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ 42.55|round }}
|
|
||||||
-> 43.0
|
|
||||||
{{ 42.55|round(1, 'floor') }}
|
|
||||||
-> 42.5
|
|
||||||
|
|
||||||
Note that even if rounded to 0 precision, a float is returned. If
|
|
||||||
you need a real integer, pipe it through `int`:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ 42.55|round|int }}
|
|
||||||
-> 43
|
|
||||||
"""
|
|
||||||
if not method in ('common', 'ceil', 'floor'):
|
|
||||||
raise FilterArgumentError('method must be common, ceil or floor')
|
|
||||||
if precision < 0:
|
|
||||||
raise FilterArgumentError('precision must be a postive integer '
|
|
||||||
'or zero.')
|
|
||||||
if method == 'common':
|
|
||||||
return round(value, precision)
|
|
||||||
func = getattr(math, method)
|
|
||||||
if precision:
|
|
||||||
return func(value * 10 * precision) / (10 * precision)
|
|
||||||
else:
|
|
||||||
return func(value)
|
|
||||||
|
|
||||||
|
|
||||||
def do_sort(value, reverse=False):
|
|
||||||
"""Sort a sequence. Per default it sorts ascending, if you pass it
|
|
||||||
true as first argument it will reverse the sorting.
|
|
||||||
"""
|
|
||||||
return sorted(value, reverse=reverse)
|
|
||||||
|
|
||||||
|
|
||||||
@environmentfilter
|
|
||||||
def do_groupby(environment, value, attribute):
|
|
||||||
"""Group a sequence of objects by a common attribute.
|
|
||||||
|
|
||||||
If you for example have a list of dicts or objects that represent persons
|
|
||||||
with `gender`, `first_name` and `last_name` attributes and you want to
|
|
||||||
group all users by genders you can do something like the following
|
|
||||||
snippet:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for group in persons|groupby('gender') %}
|
|
||||||
<li>{{ group.grouper }}<ul>
|
|
||||||
{% for person in group.list %}
|
|
||||||
<li>{{ person.first_name }} {{ person.last_name }}</li>
|
|
||||||
{% endfor %}</ul></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
Additionally it's possible to use tuple unpacking for the grouper and
|
|
||||||
list:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for grouper, list in persons|groupby('gender') %}
|
|
||||||
...
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
As you can see the item we're grouping by is stored in the `grouper`
|
|
||||||
attribute and the `list` contains all the objects that have this grouper
|
|
||||||
in common.
|
|
||||||
"""
|
|
||||||
expr = lambda x: environment.getitem(x, attribute)
|
|
||||||
return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
|
|
||||||
|
|
||||||
|
|
||||||
class _GroupTuple(tuple):
|
|
||||||
__slots__ = ()
|
|
||||||
grouper = property(itemgetter(0))
|
|
||||||
list = property(itemgetter(1))
|
|
||||||
|
|
||||||
#def __new__(cls, (key, value)):
|
|
||||||
def __new__(cls, kv):
|
|
||||||
return tuple.__new__(cls, (kv[0], list(kv[1])))
|
|
||||||
|
|
||||||
|
|
||||||
def do_list(value):
|
|
||||||
"""Convert the value into a list. If it was a string the returned list
|
|
||||||
will be a list of characters.
|
|
||||||
"""
|
|
||||||
return list(value)
|
|
||||||
|
|
||||||
|
|
||||||
def do_mark_safe(value):
|
|
||||||
"""Mark the value as safe which means that in an environment with automatic
|
|
||||||
escaping enabled this variable will not be escaped.
|
|
||||||
"""
|
|
||||||
return Markup(value)
|
|
||||||
|
|
||||||
|
|
||||||
def do_mark_unsafe(value):
|
|
||||||
"""Mark a value as unsafe. This is the reverse operation for :func:`safe`."""
|
|
||||||
return unicode(value)
|
|
||||||
|
|
||||||
|
|
||||||
def do_reverse(value):
|
|
||||||
"""Reverse the object or return an iterator the iterates over it the other
|
|
||||||
way round.
|
|
||||||
"""
|
|
||||||
if isinstance(value, basestring):
|
|
||||||
return value[::-1]
|
|
||||||
try:
|
|
||||||
return reversed(value)
|
|
||||||
except TypeError:
|
|
||||||
try:
|
|
||||||
rv = list(value)
|
|
||||||
rv.reverse()
|
|
||||||
return rv
|
|
||||||
except TypeError:
|
|
||||||
raise FilterArgumentError('argument must be iterable')
|
|
||||||
|
|
||||||
|
|
||||||
@environmentfilter
|
|
||||||
def do_attr(environment, obj, name):
|
|
||||||
"""Get an attribute of an object. ``foo|attr("bar")`` works like
|
|
||||||
``foo["bar"]`` just that always an attribute is returned and items are not
|
|
||||||
looked up.
|
|
||||||
|
|
||||||
See :ref:`Notes on subscriptions <notes-on-subscriptions>` for more details.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
name = str(name)
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
value = getattr(obj, name)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if environment.sandboxed and not \
|
|
||||||
environment.is_safe_attribute(obj, name, value):
|
|
||||||
return environment.unsafe_undefined(obj, name)
|
|
||||||
return value
|
|
||||||
return environment.undefined(obj=obj, name=name)
|
|
||||||
|
|
||||||
|
|
||||||
FILTERS = {
|
|
||||||
'attr': do_attr,
|
|
||||||
'replace': do_replace,
|
|
||||||
'upper': do_upper,
|
|
||||||
'lower': do_lower,
|
|
||||||
'escape': escape,
|
|
||||||
'e': escape,
|
|
||||||
'forceescape': do_forceescape,
|
|
||||||
'capitalize': do_capitalize,
|
|
||||||
'title': do_title,
|
|
||||||
'default': do_default,
|
|
||||||
'd': do_default,
|
|
||||||
'join': do_join,
|
|
||||||
'count': len,
|
|
||||||
'dictsort': do_dictsort,
|
|
||||||
'sort': do_sort,
|
|
||||||
'length': len,
|
|
||||||
'reverse': do_reverse,
|
|
||||||
'center': do_center,
|
|
||||||
'indent': do_indent,
|
|
||||||
'title': do_title,
|
|
||||||
'capitalize': do_capitalize,
|
|
||||||
'first': do_first,
|
|
||||||
'last': do_last,
|
|
||||||
'random': do_random,
|
|
||||||
'filesizeformat': do_filesizeformat,
|
|
||||||
'pprint': do_pprint,
|
|
||||||
'truncate': do_truncate,
|
|
||||||
'wordwrap': do_wordwrap,
|
|
||||||
'wordcount': do_wordcount,
|
|
||||||
'int': do_int,
|
|
||||||
'float': do_float,
|
|
||||||
'string': soft_unicode,
|
|
||||||
'list': do_list,
|
|
||||||
'urlize': do_urlize,
|
|
||||||
'format': do_format,
|
|
||||||
'trim': do_trim,
|
|
||||||
'striptags': do_striptags,
|
|
||||||
'slice': do_slice,
|
|
||||||
'batch': do_batch,
|
|
||||||
'sum': sum,
|
|
||||||
'abs': abs,
|
|
||||||
'round': do_round,
|
|
||||||
'sort': do_sort,
|
|
||||||
'groupby': do_groupby,
|
|
||||||
'safe': do_mark_safe,
|
|
||||||
'xmlattr': do_xmlattr
|
|
||||||
}
|
|
|
@ -1,680 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.lexer
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements a Jinja / Python combination lexer. The
|
|
||||||
`Lexer` class provided by this module is used to do some preprocessing
|
|
||||||
for Jinja.
|
|
||||||
|
|
||||||
On the one hand it filters out invalid operators like the bitshift
|
|
||||||
operators we don't allow in templates. On the other hand it separates
|
|
||||||
template code and python code in expressions.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
from operator import itemgetter
|
|
||||||
from collections import deque
|
|
||||||
from jinja2.exceptions import TemplateSyntaxError
|
|
||||||
from jinja2.utils import LRUCache, next
|
|
||||||
|
|
||||||
|
|
||||||
# cache for the lexers. Exists in order to be able to have multiple
|
|
||||||
# environments with the same lexer
|
|
||||||
_lexer_cache = LRUCache(50)
|
|
||||||
|
|
||||||
# static regular expressions
|
|
||||||
whitespace_re = re.compile(r'\s+', re.U)
|
|
||||||
string_re = re.compile(r"('([^'\\]*(?:\\.[^'\\]*)*)'"
|
|
||||||
r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S)
|
|
||||||
integer_re = re.compile(r'\d+')
|
|
||||||
|
|
||||||
# we use the unicode identifier rule if this python version is able
|
|
||||||
# to handle unicode identifiers, otherwise the standard ASCII one.
|
|
||||||
try:
|
|
||||||
compile('föö', '<unknown>', 'eval')
|
|
||||||
except SyntaxError:
|
|
||||||
name_re = re.compile(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b')
|
|
||||||
else:
|
|
||||||
from jinja2 import _stringdefs
|
|
||||||
name_re = re.compile(r'[%s][%s]*' % (_stringdefs.xid_start,
|
|
||||||
_stringdefs.xid_continue))
|
|
||||||
|
|
||||||
float_re = re.compile(r'(?<!\.)\d+\.\d+')
|
|
||||||
newline_re = re.compile(r'(\r\n|\r|\n)')
|
|
||||||
|
|
||||||
# internal the tokens and keep references to them
|
|
||||||
TOKEN_ADD = intern('add')
|
|
||||||
TOKEN_ASSIGN = intern('assign')
|
|
||||||
TOKEN_COLON = intern('colon')
|
|
||||||
TOKEN_COMMA = intern('comma')
|
|
||||||
TOKEN_DIV = intern('div')
|
|
||||||
TOKEN_DOT = intern('dot')
|
|
||||||
TOKEN_EQ = intern('eq')
|
|
||||||
TOKEN_FLOORDIV = intern('floordiv')
|
|
||||||
TOKEN_GT = intern('gt')
|
|
||||||
TOKEN_GTEQ = intern('gteq')
|
|
||||||
TOKEN_LBRACE = intern('lbrace')
|
|
||||||
TOKEN_LBRACKET = intern('lbracket')
|
|
||||||
TOKEN_LPAREN = intern('lparen')
|
|
||||||
TOKEN_LT = intern('lt')
|
|
||||||
TOKEN_LTEQ = intern('lteq')
|
|
||||||
TOKEN_MOD = intern('mod')
|
|
||||||
TOKEN_MUL = intern('mul')
|
|
||||||
TOKEN_NE = intern('ne')
|
|
||||||
TOKEN_PIPE = intern('pipe')
|
|
||||||
TOKEN_POW = intern('pow')
|
|
||||||
TOKEN_RBRACE = intern('rbrace')
|
|
||||||
TOKEN_RBRACKET = intern('rbracket')
|
|
||||||
TOKEN_RPAREN = intern('rparen')
|
|
||||||
TOKEN_SEMICOLON = intern('semicolon')
|
|
||||||
TOKEN_SUB = intern('sub')
|
|
||||||
TOKEN_TILDE = intern('tilde')
|
|
||||||
TOKEN_WHITESPACE = intern('whitespace')
|
|
||||||
TOKEN_FLOAT = intern('float')
|
|
||||||
TOKEN_INTEGER = intern('integer')
|
|
||||||
TOKEN_NAME = intern('name')
|
|
||||||
TOKEN_STRING = intern('string')
|
|
||||||
TOKEN_OPERATOR = intern('operator')
|
|
||||||
TOKEN_BLOCK_BEGIN = intern('block_begin')
|
|
||||||
TOKEN_BLOCK_END = intern('block_end')
|
|
||||||
TOKEN_VARIABLE_BEGIN = intern('variable_begin')
|
|
||||||
TOKEN_VARIABLE_END = intern('variable_end')
|
|
||||||
TOKEN_RAW_BEGIN = intern('raw_begin')
|
|
||||||
TOKEN_RAW_END = intern('raw_end')
|
|
||||||
TOKEN_COMMENT_BEGIN = intern('comment_begin')
|
|
||||||
TOKEN_COMMENT_END = intern('comment_end')
|
|
||||||
TOKEN_COMMENT = intern('comment')
|
|
||||||
TOKEN_LINESTATEMENT_BEGIN = intern('linestatement_begin')
|
|
||||||
TOKEN_LINESTATEMENT_END = intern('linestatement_end')
|
|
||||||
TOKEN_LINECOMMENT_BEGIN = intern('linecomment_begin')
|
|
||||||
TOKEN_LINECOMMENT_END = intern('linecomment_end')
|
|
||||||
TOKEN_LINECOMMENT = intern('linecomment')
|
|
||||||
TOKEN_DATA = intern('data')
|
|
||||||
TOKEN_INITIAL = intern('initial')
|
|
||||||
TOKEN_EOF = intern('eof')
|
|
||||||
|
|
||||||
# bind operators to token types
|
|
||||||
operators = {
|
|
||||||
'+': TOKEN_ADD,
|
|
||||||
'-': TOKEN_SUB,
|
|
||||||
'/': TOKEN_DIV,
|
|
||||||
'//': TOKEN_FLOORDIV,
|
|
||||||
'*': TOKEN_MUL,
|
|
||||||
'%': TOKEN_MOD,
|
|
||||||
'**': TOKEN_POW,
|
|
||||||
'~': TOKEN_TILDE,
|
|
||||||
'[': TOKEN_LBRACKET,
|
|
||||||
']': TOKEN_RBRACKET,
|
|
||||||
'(': TOKEN_LPAREN,
|
|
||||||
')': TOKEN_RPAREN,
|
|
||||||
'{': TOKEN_LBRACE,
|
|
||||||
'}': TOKEN_RBRACE,
|
|
||||||
'==': TOKEN_EQ,
|
|
||||||
'!=': TOKEN_NE,
|
|
||||||
'>': TOKEN_GT,
|
|
||||||
'>=': TOKEN_GTEQ,
|
|
||||||
'<': TOKEN_LT,
|
|
||||||
'<=': TOKEN_LTEQ,
|
|
||||||
'=': TOKEN_ASSIGN,
|
|
||||||
'.': TOKEN_DOT,
|
|
||||||
':': TOKEN_COLON,
|
|
||||||
'|': TOKEN_PIPE,
|
|
||||||
',': TOKEN_COMMA,
|
|
||||||
';': TOKEN_SEMICOLON
|
|
||||||
}
|
|
||||||
|
|
||||||
reverse_operators = dict([(v, k) for k, v in operators.iteritems()])
|
|
||||||
assert len(operators) == len(reverse_operators), 'operators dropped'
|
|
||||||
operator_re = re.compile('(%s)' % '|'.join(re.escape(x) for x in
|
|
||||||
sorted(operators, key=lambda x: -len(x))))
|
|
||||||
|
|
||||||
ignored_tokens = frozenset([TOKEN_COMMENT_BEGIN, TOKEN_COMMENT,
|
|
||||||
TOKEN_COMMENT_END, TOKEN_WHITESPACE,
|
|
||||||
TOKEN_WHITESPACE, TOKEN_LINECOMMENT_BEGIN,
|
|
||||||
TOKEN_LINECOMMENT_END, TOKEN_LINECOMMENT])
|
|
||||||
ignore_if_empty = frozenset([TOKEN_WHITESPACE, TOKEN_DATA,
|
|
||||||
TOKEN_COMMENT, TOKEN_LINECOMMENT])
|
|
||||||
|
|
||||||
|
|
||||||
def _describe_token_type(token_type):
|
|
||||||
if token_type in reverse_operators:
|
|
||||||
return reverse_operators[token_type]
|
|
||||||
return {
|
|
||||||
TOKEN_COMMENT_BEGIN: 'begin of comment',
|
|
||||||
TOKEN_COMMENT_END: 'end of comment',
|
|
||||||
TOKEN_COMMENT: 'comment',
|
|
||||||
TOKEN_LINECOMMENT: 'comment',
|
|
||||||
TOKEN_BLOCK_BEGIN: 'begin of statement block',
|
|
||||||
TOKEN_BLOCK_END: 'end of statement block',
|
|
||||||
TOKEN_VARIABLE_BEGIN: 'begin of print statement',
|
|
||||||
TOKEN_VARIABLE_END: 'end of print statement',
|
|
||||||
TOKEN_LINESTATEMENT_BEGIN: 'begin of line statement',
|
|
||||||
TOKEN_LINESTATEMENT_END: 'end of line statement',
|
|
||||||
TOKEN_DATA: 'template data / text',
|
|
||||||
TOKEN_EOF: 'end of template'
|
|
||||||
}.get(token_type, token_type)
|
|
||||||
|
|
||||||
|
|
||||||
def describe_token(token):
|
|
||||||
"""Returns a description of the token."""
|
|
||||||
if token.type == 'name':
|
|
||||||
return token.value
|
|
||||||
return _describe_token_type(token.type)
|
|
||||||
|
|
||||||
|
|
||||||
def describe_token_expr(expr):
|
|
||||||
"""Like `describe_token` but for token expressions."""
|
|
||||||
if ':' in expr:
|
|
||||||
type, value = expr.split(':', 1)
|
|
||||||
if type == 'name':
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
type = expr
|
|
||||||
return _describe_token_type(type)
|
|
||||||
|
|
||||||
|
|
||||||
def count_newlines(value):
|
|
||||||
"""Count the number of newline characters in the string. This is
|
|
||||||
useful for extensions that filter a stream.
|
|
||||||
"""
|
|
||||||
return len(newline_re.findall(value))
|
|
||||||
|
|
||||||
|
|
||||||
def compile_rules(environment):
|
|
||||||
"""Compiles all the rules from the environment into a list of rules."""
|
|
||||||
e = re.escape
|
|
||||||
rules = [
|
|
||||||
(len(environment.comment_start_string), 'comment',
|
|
||||||
e(environment.comment_start_string)),
|
|
||||||
(len(environment.block_start_string), 'block',
|
|
||||||
e(environment.block_start_string)),
|
|
||||||
(len(environment.variable_start_string), 'variable',
|
|
||||||
e(environment.variable_start_string))
|
|
||||||
]
|
|
||||||
|
|
||||||
if environment.line_statement_prefix is not None:
|
|
||||||
rules.append((len(environment.line_statement_prefix), 'linestatement',
|
|
||||||
r'^\s*' + e(environment.line_statement_prefix)))
|
|
||||||
if environment.line_comment_prefix is not None:
|
|
||||||
rules.append((len(environment.line_comment_prefix), 'linecomment',
|
|
||||||
r'(?:^|(?<=\S))[^\S\r\n]*' +
|
|
||||||
e(environment.line_comment_prefix)))
|
|
||||||
|
|
||||||
return [x[1:] for x in sorted(rules, reverse=True)]
|
|
||||||
|
|
||||||
|
|
||||||
class Failure(object):
|
|
||||||
"""Class that raises a `TemplateSyntaxError` if called.
|
|
||||||
Used by the `Lexer` to specify known errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message, cls=TemplateSyntaxError):
|
|
||||||
self.message = message
|
|
||||||
self.error_class = cls
|
|
||||||
|
|
||||||
def __call__(self, lineno, filename):
|
|
||||||
raise self.error_class(self.message, lineno, filename)
|
|
||||||
|
|
||||||
|
|
||||||
class Token(tuple):
|
|
||||||
"""Token class."""
|
|
||||||
__slots__ = ()
|
|
||||||
lineno, type, value = (property(itemgetter(x)) for x in range(3))
|
|
||||||
|
|
||||||
def __new__(cls, lineno, type, value):
|
|
||||||
return tuple.__new__(cls, (lineno, intern(str(type)), value))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.type in reverse_operators:
|
|
||||||
return reverse_operators[self.type]
|
|
||||||
elif self.type == 'name':
|
|
||||||
return self.value
|
|
||||||
return self.type
|
|
||||||
|
|
||||||
def test(self, expr):
|
|
||||||
"""Test a token against a token expression. This can either be a
|
|
||||||
token type or ``'token_type:token_value'``. This can only test
|
|
||||||
against string values and types.
|
|
||||||
"""
|
|
||||||
# here we do a regular string equality check as test_any is usually
|
|
||||||
# passed an iterable of not interned strings.
|
|
||||||
if self.type == expr:
|
|
||||||
return True
|
|
||||||
elif ':' in expr:
|
|
||||||
return expr.split(':', 1) == [self.type, self.value]
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_any(self, *iterable):
|
|
||||||
"""Test against multiple token expressions."""
|
|
||||||
for expr in iterable:
|
|
||||||
if self.test(expr):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Token(%r, %r, %r)' % (
|
|
||||||
self.lineno,
|
|
||||||
self.type,
|
|
||||||
self.value
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TokenStreamIterator(object):
|
|
||||||
"""The iterator for tokenstreams. Iterate over the stream
|
|
||||||
until the eof token is reached.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
token = self.stream.current
|
|
||||||
if token.type is TOKEN_EOF:
|
|
||||||
self.stream.close()
|
|
||||||
raise StopIteration()
|
|
||||||
next(self.stream)
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
class TokenStream(object):
|
|
||||||
"""A token stream is an iterable that yields :class:`Token`\s. The
|
|
||||||
parser however does not iterate over it but calls :meth:`next` to go
|
|
||||||
one token ahead. The current active token is stored as :attr:`current`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, generator, name, filename):
|
|
||||||
self._next = iter(generator).next
|
|
||||||
self._pushed = deque()
|
|
||||||
self.name = name
|
|
||||||
self.filename = filename
|
|
||||||
self.closed = False
|
|
||||||
self.current = Token(1, TOKEN_INITIAL, '')
|
|
||||||
next(self)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return TokenStreamIterator(self)
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
return bool(self._pushed) or self.current.type is not TOKEN_EOF
|
|
||||||
|
|
||||||
eos = property(lambda x: not x, doc="Are we at the end of the stream?")
|
|
||||||
|
|
||||||
def push(self, token):
|
|
||||||
"""Push a token back to the stream."""
|
|
||||||
self._pushed.append(token)
|
|
||||||
|
|
||||||
def look(self):
|
|
||||||
"""Look at the next token."""
|
|
||||||
old_token = next(self)
|
|
||||||
result = self.current
|
|
||||||
self.push(result)
|
|
||||||
self.current = old_token
|
|
||||||
return result
|
|
||||||
|
|
||||||
def skip(self, n=1):
|
|
||||||
"""Got n tokens ahead."""
|
|
||||||
for x in xrange(n):
|
|
||||||
next(self)
|
|
||||||
|
|
||||||
def next_if(self, expr):
|
|
||||||
"""Perform the token test and return the token if it matched.
|
|
||||||
Otherwise the return value is `None`.
|
|
||||||
"""
|
|
||||||
if self.current.test(expr):
|
|
||||||
return next(self)
|
|
||||||
|
|
||||||
def skip_if(self, expr):
|
|
||||||
"""Like :meth:`next_if` but only returns `True` or `False`."""
|
|
||||||
return self.next_if(expr) is not None
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
"""Go one token ahead and return the old one"""
|
|
||||||
rv = self.current
|
|
||||||
if self._pushed:
|
|
||||||
self.current = self._pushed.popleft()
|
|
||||||
elif self.current.type is not TOKEN_EOF:
|
|
||||||
try:
|
|
||||||
self.current = self._next()
|
|
||||||
except StopIteration:
|
|
||||||
self.close()
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Close the stream."""
|
|
||||||
self.current = Token(self.current.lineno, TOKEN_EOF, '')
|
|
||||||
self._next = None
|
|
||||||
self.closed = True
|
|
||||||
|
|
||||||
def expect(self, expr):
|
|
||||||
"""Expect a given token type and return it. This accepts the same
|
|
||||||
argument as :meth:`jinja2.lexer.Token.test`.
|
|
||||||
"""
|
|
||||||
if not self.current.test(expr):
|
|
||||||
expr = describe_token_expr(expr)
|
|
||||||
if self.current.type is TOKEN_EOF:
|
|
||||||
raise TemplateSyntaxError('unexpected end of template, '
|
|
||||||
'expected %r.' % expr,
|
|
||||||
self.current.lineno,
|
|
||||||
self.name, self.filename)
|
|
||||||
raise TemplateSyntaxError("expected token %r, got %r" %
|
|
||||||
(expr, describe_token(self.current)),
|
|
||||||
self.current.lineno,
|
|
||||||
self.name, self.filename)
|
|
||||||
try:
|
|
||||||
return self.current
|
|
||||||
finally:
|
|
||||||
next(self)
|
|
||||||
|
|
||||||
|
|
||||||
def get_lexer(environment):
|
|
||||||
"""Return a lexer which is probably cached."""
|
|
||||||
key = (environment.block_start_string,
|
|
||||||
environment.block_end_string,
|
|
||||||
environment.variable_start_string,
|
|
||||||
environment.variable_end_string,
|
|
||||||
environment.comment_start_string,
|
|
||||||
environment.comment_end_string,
|
|
||||||
environment.line_statement_prefix,
|
|
||||||
environment.line_comment_prefix,
|
|
||||||
environment.trim_blocks,
|
|
||||||
environment.newline_sequence)
|
|
||||||
lexer = _lexer_cache.get(key)
|
|
||||||
if lexer is None:
|
|
||||||
lexer = Lexer(environment)
|
|
||||||
_lexer_cache[key] = lexer
|
|
||||||
return lexer
|
|
||||||
|
|
||||||
|
|
||||||
class Lexer(object):
|
|
||||||
"""Class that implements a lexer for a given environment. Automatically
|
|
||||||
created by the environment class, usually you don't have to do that.
|
|
||||||
|
|
||||||
Note that the lexer is not automatically bound to an environment.
|
|
||||||
Multiple environments can share the same lexer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, environment):
|
|
||||||
# shortcuts
|
|
||||||
c = lambda x: re.compile(x, re.M | re.S)
|
|
||||||
e = re.escape
|
|
||||||
|
|
||||||
# lexing rules for tags
|
|
||||||
tag_rules = [
|
|
||||||
(whitespace_re, TOKEN_WHITESPACE, None),
|
|
||||||
(float_re, TOKEN_FLOAT, None),
|
|
||||||
(integer_re, TOKEN_INTEGER, None),
|
|
||||||
(name_re, TOKEN_NAME, None),
|
|
||||||
(string_re, TOKEN_STRING, None),
|
|
||||||
(operator_re, TOKEN_OPERATOR, None)
|
|
||||||
]
|
|
||||||
|
|
||||||
# assamble the root lexing rule. because "|" is ungreedy
|
|
||||||
# we have to sort by length so that the lexer continues working
|
|
||||||
# as expected when we have parsing rules like <% for block and
|
|
||||||
# <%= for variables. (if someone wants asp like syntax)
|
|
||||||
# variables are just part of the rules if variable processing
|
|
||||||
# is required.
|
|
||||||
root_tag_rules = compile_rules(environment)
|
|
||||||
|
|
||||||
# block suffix if trimming is enabled
|
|
||||||
block_suffix_re = environment.trim_blocks and '\\n?' or ''
|
|
||||||
|
|
||||||
self.newline_sequence = environment.newline_sequence
|
|
||||||
|
|
||||||
# global lexing rules
|
|
||||||
self.rules = {
|
|
||||||
'root': [
|
|
||||||
# directives
|
|
||||||
(c('(.*?)(?:%s)' % '|'.join(
|
|
||||||
[r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*%s)' % (
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_end_string)
|
|
||||||
)] + [
|
|
||||||
r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, r)
|
|
||||||
for n, r in root_tag_rules
|
|
||||||
])), (TOKEN_DATA, '#bygroup'), '#bygroup'),
|
|
||||||
# data
|
|
||||||
(c('.+'), TOKEN_DATA, None)
|
|
||||||
],
|
|
||||||
# comments
|
|
||||||
TOKEN_COMMENT_BEGIN: [
|
|
||||||
(c(r'(.*?)((?:\-%s\s*|%s)%s)' % (
|
|
||||||
e(environment.comment_end_string),
|
|
||||||
e(environment.comment_end_string),
|
|
||||||
block_suffix_re
|
|
||||||
)), (TOKEN_COMMENT, TOKEN_COMMENT_END), '#pop'),
|
|
||||||
(c('(.)'), (Failure('Missing end of comment tag'),), None)
|
|
||||||
],
|
|
||||||
# blocks
|
|
||||||
TOKEN_BLOCK_BEGIN: [
|
|
||||||
(c('(?:\-%s\s*|%s)%s' % (
|
|
||||||
e(environment.block_end_string),
|
|
||||||
e(environment.block_end_string),
|
|
||||||
block_suffix_re
|
|
||||||
)), TOKEN_BLOCK_END, '#pop'),
|
|
||||||
] + tag_rules,
|
|
||||||
# variables
|
|
||||||
TOKEN_VARIABLE_BEGIN: [
|
|
||||||
(c('\-%s\s*|%s' % (
|
|
||||||
e(environment.variable_end_string),
|
|
||||||
e(environment.variable_end_string)
|
|
||||||
)), TOKEN_VARIABLE_END, '#pop')
|
|
||||||
] + tag_rules,
|
|
||||||
# raw block
|
|
||||||
TOKEN_RAW_BEGIN: [
|
|
||||||
(c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % (
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_end_string),
|
|
||||||
e(environment.block_end_string),
|
|
||||||
block_suffix_re
|
|
||||||
)), (TOKEN_DATA, TOKEN_RAW_END), '#pop'),
|
|
||||||
(c('(.)'), (Failure('Missing end of raw directive'),), None)
|
|
||||||
],
|
|
||||||
# line statements
|
|
||||||
TOKEN_LINESTATEMENT_BEGIN: [
|
|
||||||
(c(r'\s*(\n|$)'), TOKEN_LINESTATEMENT_END, '#pop')
|
|
||||||
] + tag_rules,
|
|
||||||
# line comments
|
|
||||||
TOKEN_LINECOMMENT_BEGIN: [
|
|
||||||
(c(r'(.*?)()(?=\n|$)'), (TOKEN_LINECOMMENT,
|
|
||||||
TOKEN_LINECOMMENT_END), '#pop')
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def _normalize_newlines(self, value):
|
|
||||||
"""Called for strings and template data to normlize it to unicode."""
|
|
||||||
return newline_re.sub(self.newline_sequence, value)
|
|
||||||
|
|
||||||
def tokenize(self, source, name=None, filename=None, state=None):
|
|
||||||
"""Calls tokeniter + tokenize and wraps it in a token stream.
|
|
||||||
"""
|
|
||||||
stream = self.tokeniter(source, name, filename, state)
|
|
||||||
return TokenStream(self.wrap(stream, name, filename), name, filename)
|
|
||||||
|
|
||||||
def wrap(self, stream, name=None, filename=None):
|
|
||||||
"""This is called with the stream as returned by `tokenize` and wraps
|
|
||||||
every token in a :class:`Token` and converts the value.
|
|
||||||
"""
|
|
||||||
for lineno, token, value in stream:
|
|
||||||
if token in ignored_tokens:
|
|
||||||
continue
|
|
||||||
elif token == 'linestatement_begin':
|
|
||||||
token = 'block_begin'
|
|
||||||
elif token == 'linestatement_end':
|
|
||||||
token = 'block_end'
|
|
||||||
# we are not interested in those tokens in the parser
|
|
||||||
elif token in ('raw_begin', 'raw_end'):
|
|
||||||
continue
|
|
||||||
elif token == 'data':
|
|
||||||
value = self._normalize_newlines(value)
|
|
||||||
elif token == 'keyword':
|
|
||||||
token = value
|
|
||||||
elif token == 'name':
|
|
||||||
value = str(value)
|
|
||||||
elif token == 'string':
|
|
||||||
# try to unescape string
|
|
||||||
try:
|
|
||||||
value = self._normalize_newlines(value[1:-1]) \
|
|
||||||
.encode('ascii', 'backslashreplace') \
|
|
||||||
.decode('unicode-escape')
|
|
||||||
except Exception as e:
|
|
||||||
msg = str(e).split(':')[-1].strip()
|
|
||||||
raise TemplateSyntaxError(msg, lineno, name, filename)
|
|
||||||
# if we can express it as bytestring (ascii only)
|
|
||||||
# we do that for support of semi broken APIs
|
|
||||||
# as datetime.datetime.strftime. On python 3 this
|
|
||||||
# call becomes a noop thanks to 2to3
|
|
||||||
try:
|
|
||||||
value = str(value)
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
elif token == 'integer':
|
|
||||||
value = int(value)
|
|
||||||
elif token == 'float':
|
|
||||||
value = float(value)
|
|
||||||
elif token == 'operator':
|
|
||||||
token = operators[value]
|
|
||||||
yield Token(lineno, token, value)
|
|
||||||
|
|
||||||
def tokeniter(self, source, name, filename=None, state=None):
|
|
||||||
"""This method tokenizes the text and returns the tokens in a
|
|
||||||
generator. Use this method if you just want to tokenize a template.
|
|
||||||
"""
|
|
||||||
source = '\n'.join(unicode(source).splitlines())
|
|
||||||
pos = 0
|
|
||||||
lineno = 1
|
|
||||||
stack = ['root']
|
|
||||||
if state is not None and state != 'root':
|
|
||||||
assert state in ('variable', 'block'), 'invalid state'
|
|
||||||
stack.append(state + '_begin')
|
|
||||||
else:
|
|
||||||
state = 'root'
|
|
||||||
statetokens = self.rules[stack[-1]]
|
|
||||||
source_length = len(source)
|
|
||||||
|
|
||||||
balancing_stack = []
|
|
||||||
|
|
||||||
while 1:
|
|
||||||
# tokenizer loop
|
|
||||||
for regex, tokens, new_state in statetokens:
|
|
||||||
m = regex.match(source, pos)
|
|
||||||
# if no match we try again with the next rule
|
|
||||||
if m is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# we only match blocks and variables if brances / parentheses
|
|
||||||
# are balanced. continue parsing with the lower rule which
|
|
||||||
# is the operator rule. do this only if the end tags look
|
|
||||||
# like operators
|
|
||||||
if balancing_stack and \
|
|
||||||
tokens in ('variable_end', 'block_end',
|
|
||||||
'linestatement_end'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# tuples support more options
|
|
||||||
if isinstance(tokens, tuple):
|
|
||||||
for idx, token in enumerate(tokens):
|
|
||||||
# failure group
|
|
||||||
if token.__class__ is Failure:
|
|
||||||
raise token(lineno, filename)
|
|
||||||
# bygroup is a bit more complex, in that case we
|
|
||||||
# yield for the current token the first named
|
|
||||||
# group that matched
|
|
||||||
elif token == '#bygroup':
|
|
||||||
for key, value in m.groupdict().iteritems():
|
|
||||||
if value is not None:
|
|
||||||
yield lineno, key, value
|
|
||||||
lineno += value.count('\n')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise RuntimeError('%r wanted to resolve '
|
|
||||||
'the token dynamically'
|
|
||||||
' but no group matched'
|
|
||||||
% regex)
|
|
||||||
# normal group
|
|
||||||
else:
|
|
||||||
data = m.group(idx + 1)
|
|
||||||
if data or token not in ignore_if_empty:
|
|
||||||
yield lineno, token, data
|
|
||||||
lineno += data.count('\n')
|
|
||||||
|
|
||||||
# strings as token just are yielded as it.
|
|
||||||
else:
|
|
||||||
data = m.group()
|
|
||||||
# update brace/parentheses balance
|
|
||||||
if tokens == 'operator':
|
|
||||||
if data == '{':
|
|
||||||
balancing_stack.append('}')
|
|
||||||
elif data == '(':
|
|
||||||
balancing_stack.append(')')
|
|
||||||
elif data == '[':
|
|
||||||
balancing_stack.append(']')
|
|
||||||
elif data in ('}', ')', ']'):
|
|
||||||
if not balancing_stack:
|
|
||||||
raise TemplateSyntaxError('unexpected \'%s\'' %
|
|
||||||
data, lineno, name,
|
|
||||||
filename)
|
|
||||||
expected_op = balancing_stack.pop()
|
|
||||||
if expected_op != data:
|
|
||||||
raise TemplateSyntaxError('unexpected \'%s\', '
|
|
||||||
'expected \'%s\'' %
|
|
||||||
(data, expected_op),
|
|
||||||
lineno, name,
|
|
||||||
filename)
|
|
||||||
# yield items
|
|
||||||
if data or tokens not in ignore_if_empty:
|
|
||||||
yield lineno, tokens, data
|
|
||||||
lineno += data.count('\n')
|
|
||||||
|
|
||||||
# fetch new position into new variable so that we can check
|
|
||||||
# if there is a internal parsing error which would result
|
|
||||||
# in an infinite loop
|
|
||||||
pos2 = m.end()
|
|
||||||
|
|
||||||
# handle state changes
|
|
||||||
if new_state is not None:
|
|
||||||
# remove the uppermost state
|
|
||||||
if new_state == '#pop':
|
|
||||||
stack.pop()
|
|
||||||
# resolve the new state by group checking
|
|
||||||
elif new_state == '#bygroup':
|
|
||||||
for key, value in m.groupdict().iteritems():
|
|
||||||
if value is not None:
|
|
||||||
stack.append(key)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise RuntimeError('%r wanted to resolve the '
|
|
||||||
'new state dynamically but'
|
|
||||||
' no group matched' %
|
|
||||||
regex)
|
|
||||||
# direct state name given
|
|
||||||
else:
|
|
||||||
stack.append(new_state)
|
|
||||||
statetokens = self.rules[stack[-1]]
|
|
||||||
# we are still at the same position and no stack change.
|
|
||||||
# this means a loop without break condition, avoid that and
|
|
||||||
# raise error
|
|
||||||
elif pos2 == pos:
|
|
||||||
raise RuntimeError('%r yielded empty string without '
|
|
||||||
'stack change' % regex)
|
|
||||||
# publish new function and start again
|
|
||||||
pos = pos2
|
|
||||||
break
|
|
||||||
# if loop terminated without break we havn't found a single match
|
|
||||||
# either we are at the end of the file or we have a problem
|
|
||||||
else:
|
|
||||||
# end of text
|
|
||||||
if pos >= source_length:
|
|
||||||
return
|
|
||||||
# something went wrong
|
|
||||||
raise TemplateSyntaxError('unexpected char %r at %d' %
|
|
||||||
(source[pos], pos), lineno,
|
|
||||||
name, filename)
|
|
|
@ -1,449 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.loaders
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja loader classes.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import weakref
|
|
||||||
from types import ModuleType
|
|
||||||
from os import path
|
|
||||||
try:
|
|
||||||
from hashlib import sha1
|
|
||||||
except ImportError:
|
|
||||||
from sha import new as sha1
|
|
||||||
from jinja2.exceptions import TemplateNotFound
|
|
||||||
from jinja2.utils import LRUCache, open_if_exists, internalcode
|
|
||||||
|
|
||||||
|
|
||||||
def split_template_path(template):
|
|
||||||
"""Split a path into segments and perform a sanity check. If it detects
|
|
||||||
'..' in the path it will raise a `TemplateNotFound` error.
|
|
||||||
"""
|
|
||||||
pieces = []
|
|
||||||
for piece in template.split('/'):
|
|
||||||
if path.sep in piece \
|
|
||||||
or (path.altsep and path.altsep in piece) or \
|
|
||||||
piece == path.pardir:
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
elif piece and piece != '.':
|
|
||||||
pieces.append(piece)
|
|
||||||
return pieces
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLoader(object):
|
|
||||||
"""Baseclass for all loaders. Subclass this and override `get_source` to
|
|
||||||
implement a custom loading mechanism. The environment provides a
|
|
||||||
`get_template` method that calls the loader's `load` method to get the
|
|
||||||
:class:`Template` object.
|
|
||||||
|
|
||||||
A very basic example for a loader that looks up templates on the file
|
|
||||||
system could look like this::
|
|
||||||
|
|
||||||
from jinja2 import BaseLoader, TemplateNotFound
|
|
||||||
from os.path import join, exists, getmtime
|
|
||||||
|
|
||||||
class MyLoader(BaseLoader):
|
|
||||||
|
|
||||||
def __init__(self, path):
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
path = join(self.path, template)
|
|
||||||
if not exists(path):
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
mtime = getmtime(path)
|
|
||||||
with file(path) as f:
|
|
||||||
source = f.read().decode('utf-8')
|
|
||||||
return source, path, lambda: mtime == getmtime(path)
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: if set to `False` it indicates that the loader cannot provide access
|
|
||||||
#: to the source of templates.
|
|
||||||
#:
|
|
||||||
#: .. versionadded:: 2.4
|
|
||||||
has_source_access = True
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
"""Get the template source, filename and reload helper for a template.
|
|
||||||
It's passed the environment and template name and has to return a
|
|
||||||
tuple in the form ``(source, filename, uptodate)`` or raise a
|
|
||||||
`TemplateNotFound` error if it can't locate the template.
|
|
||||||
|
|
||||||
The source part of the returned tuple must be the source of the
|
|
||||||
template as unicode string or a ASCII bytestring. The filename should
|
|
||||||
be the name of the file on the filesystem if it was loaded from there,
|
|
||||||
otherwise `None`. The filename is used by python for the tracebacks
|
|
||||||
if no loader extension is used.
|
|
||||||
|
|
||||||
The last item in the tuple is the `uptodate` function. If auto
|
|
||||||
reloading is enabled it's always called to check if the template
|
|
||||||
changed. No arguments are passed so the function must store the
|
|
||||||
old state somewhere (for example in a closure). If it returns `False`
|
|
||||||
the template will be reloaded.
|
|
||||||
"""
|
|
||||||
if not self.has_source_access:
|
|
||||||
raise RuntimeError('%s cannot provide access to the source' %
|
|
||||||
self.__class__.__name__)
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
||||||
def list_templates(self):
|
|
||||||
"""Iterates over all templates. If the loader does not support that
|
|
||||||
it should raise a :exc:`TypeError` which is the default behavior.
|
|
||||||
"""
|
|
||||||
raise TypeError('this loader cannot iterate over all templates')
|
|
||||||
|
|
||||||
@internalcode
|
|
||||||
def load(self, environment, name, globals=None):
|
|
||||||
"""Loads a template. This method looks up the template in the cache
|
|
||||||
or loads one by calling :meth:`get_source`. Subclasses should not
|
|
||||||
override this method as loaders working on collections of other
|
|
||||||
loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
|
|
||||||
will not call this method but `get_source` directly.
|
|
||||||
"""
|
|
||||||
code = None
|
|
||||||
if globals is None:
|
|
||||||
globals = {}
|
|
||||||
|
|
||||||
# first we try to get the source for this template together
|
|
||||||
# with the filename and the uptodate function.
|
|
||||||
source, filename, uptodate = self.get_source(environment, name)
|
|
||||||
|
|
||||||
# try to load the code from the bytecode cache if there is a
|
|
||||||
# bytecode cache configured.
|
|
||||||
bcc = environment.bytecode_cache
|
|
||||||
if bcc is not None:
|
|
||||||
bucket = bcc.get_bucket(environment, name, filename, source)
|
|
||||||
code = bucket.code
|
|
||||||
|
|
||||||
# if we don't have code so far (not cached, no longer up to
|
|
||||||
# date) etc. we compile the template
|
|
||||||
if code is None:
|
|
||||||
code = environment.compile(source, name, filename)
|
|
||||||
|
|
||||||
# if the bytecode cache is available and the bucket doesn't
|
|
||||||
# have a code so far, we give the bucket the new code and put
|
|
||||||
# it back to the bytecode cache.
|
|
||||||
if bcc is not None and bucket.code is None:
|
|
||||||
bucket.code = code
|
|
||||||
bcc.set_bucket(bucket)
|
|
||||||
|
|
||||||
return environment.template_class.from_code(environment, code,
|
|
||||||
globals, uptodate)
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystemLoader(BaseLoader):
|
|
||||||
"""Loads templates from the file system. This loader can find templates
|
|
||||||
in folders on the file system and is the preferred way to load them.
|
|
||||||
|
|
||||||
The loader takes the path to the templates as string, or if multiple
|
|
||||||
locations are wanted a list of them which is then looked up in the
|
|
||||||
given order:
|
|
||||||
|
|
||||||
>>> loader = FileSystemLoader('/path/to/templates')
|
|
||||||
>>> loader = FileSystemLoader(['/path/to/templates', '/other/path'])
|
|
||||||
|
|
||||||
Per default the template encoding is ``'utf-8'`` which can be changed
|
|
||||||
by setting the `encoding` parameter to something else.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, searchpath, encoding='utf-8'):
|
|
||||||
if isinstance(searchpath, basestring):
|
|
||||||
searchpath = [searchpath]
|
|
||||||
self.searchpath = list(searchpath)
|
|
||||||
self.encoding = encoding
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
pieces = split_template_path(template)
|
|
||||||
for searchpath in self.searchpath:
|
|
||||||
filename = path.join(searchpath, *pieces)
|
|
||||||
f = open_if_exists(filename)
|
|
||||||
if f is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
contents = f.read().decode(self.encoding)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
mtime = path.getmtime(filename)
|
|
||||||
def uptodate():
|
|
||||||
try:
|
|
||||||
return path.getmtime(filename) == mtime
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
return contents, filename, uptodate
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
||||||
def list_templates(self):
|
|
||||||
found = set()
|
|
||||||
for searchpath in self.searchpath:
|
|
||||||
for dirpath, dirnames, filenames in os.walk(searchpath):
|
|
||||||
for filename in filenames:
|
|
||||||
template = os.path.join(dirpath, filename) \
|
|
||||||
[len(searchpath):].strip(os.path.sep) \
|
|
||||||
.replace(os.path.sep, '/')
|
|
||||||
if template[:2] == './':
|
|
||||||
template = template[2:]
|
|
||||||
if template not in found:
|
|
||||||
found.add(template)
|
|
||||||
return sorted(found)
|
|
||||||
|
|
||||||
|
|
||||||
class PackageLoader(BaseLoader):
|
|
||||||
"""Load templates from python eggs or packages. It is constructed with
|
|
||||||
the name of the python package and the path to the templates in that
|
|
||||||
package::
|
|
||||||
|
|
||||||
loader = PackageLoader('mypackage', 'views')
|
|
||||||
|
|
||||||
If the package path is not given, ``'templates'`` is assumed.
|
|
||||||
|
|
||||||
Per default the template encoding is ``'utf-8'`` which can be changed
|
|
||||||
by setting the `encoding` parameter to something else. Due to the nature
|
|
||||||
of eggs it's only possible to reload templates if the package was loaded
|
|
||||||
from the file system and not a zip file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, package_name, package_path='templates',
|
|
||||||
encoding='utf-8'):
|
|
||||||
from pkg_resources import DefaultProvider, ResourceManager, \
|
|
||||||
get_provider
|
|
||||||
provider = get_provider(package_name)
|
|
||||||
self.encoding = encoding
|
|
||||||
self.manager = ResourceManager()
|
|
||||||
self.filesystem_bound = isinstance(provider, DefaultProvider)
|
|
||||||
self.provider = provider
|
|
||||||
self.package_path = package_path
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
pieces = split_template_path(template)
|
|
||||||
p = '/'.join((self.package_path,) + tuple(pieces))
|
|
||||||
if not self.provider.has_resource(p):
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
||||||
filename = uptodate = None
|
|
||||||
if self.filesystem_bound:
|
|
||||||
filename = self.provider.get_resource_filename(self.manager, p)
|
|
||||||
mtime = path.getmtime(filename)
|
|
||||||
def uptodate():
|
|
||||||
try:
|
|
||||||
return path.getmtime(filename) == mtime
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
source = self.provider.get_resource_string(self.manager, p)
|
|
||||||
return source.decode(self.encoding), filename, uptodate
|
|
||||||
|
|
||||||
def list_templates(self):
|
|
||||||
path = self.package_path
|
|
||||||
if path[:2] == './':
|
|
||||||
path = path[2:]
|
|
||||||
elif path == '.':
|
|
||||||
path = ''
|
|
||||||
offset = len(path)
|
|
||||||
results = []
|
|
||||||
def _walk(path):
|
|
||||||
for filename in self.provider.resource_listdir(path):
|
|
||||||
fullname = path + '/' + filename
|
|
||||||
if self.provider.resource_isdir(fullname):
|
|
||||||
for item in _walk(fullname):
|
|
||||||
results.append(item)
|
|
||||||
else:
|
|
||||||
results.append(fullname[offset:].lstrip('/'))
|
|
||||||
_walk(path)
|
|
||||||
results.sort()
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
class DictLoader(BaseLoader):
|
|
||||||
"""Loads a template from a python dict. It's passed a dict of unicode
|
|
||||||
strings bound to template names. This loader is useful for unittesting:
|
|
||||||
|
|
||||||
>>> loader = DictLoader({'index.html': 'source here'})
|
|
||||||
|
|
||||||
Because auto reloading is rarely useful this is disabled per default.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, mapping):
|
|
||||||
self.mapping = mapping
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
if template in self.mapping:
|
|
||||||
source = self.mapping[template]
|
|
||||||
return source, None, lambda: source != self.mapping.get(template)
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
||||||
def list_templates(self):
|
|
||||||
return sorted(self.mapping)
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionLoader(BaseLoader):
|
|
||||||
"""A loader that is passed a function which does the loading. The
|
|
||||||
function becomes the name of the template passed and has to return either
|
|
||||||
an unicode string with the template source, a tuple in the form ``(source,
|
|
||||||
filename, uptodatefunc)`` or `None` if the template does not exist.
|
|
||||||
|
|
||||||
>>> def load_template(name):
|
|
||||||
... if name == 'index.html':
|
|
||||||
... return '...'
|
|
||||||
...
|
|
||||||
>>> loader = FunctionLoader(load_template)
|
|
||||||
|
|
||||||
The `uptodatefunc` is a function that is called if autoreload is enabled
|
|
||||||
and has to return `True` if the template is still up to date. For more
|
|
||||||
details have a look at :meth:`BaseLoader.get_source` which has the same
|
|
||||||
return value.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, load_func):
|
|
||||||
self.load_func = load_func
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
rv = self.load_func(template)
|
|
||||||
if rv is None:
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
elif isinstance(rv, basestring):
|
|
||||||
return rv, None, None
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixLoader(BaseLoader):
|
|
||||||
"""A loader that is passed a dict of loaders where each loader is bound
|
|
||||||
to a prefix. The prefix is delimited from the template by a slash per
|
|
||||||
default, which can be changed by setting the `delimiter` argument to
|
|
||||||
something else::
|
|
||||||
|
|
||||||
loader = PrefixLoader({
|
|
||||||
'app1': PackageLoader('mypackage.app1'),
|
|
||||||
'app2': PackageLoader('mypackage.app2')
|
|
||||||
})
|
|
||||||
|
|
||||||
By loading ``'app1/index.html'`` the file from the app1 package is loaded,
|
|
||||||
by loading ``'app2/index.html'`` the file from the second.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, mapping, delimiter='/'):
|
|
||||||
self.mapping = mapping
|
|
||||||
self.delimiter = delimiter
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
try:
|
|
||||||
prefix, name = template.split(self.delimiter, 1)
|
|
||||||
loader = self.mapping[prefix]
|
|
||||||
except (ValueError, KeyError):
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
try:
|
|
||||||
return loader.get_source(environment, name)
|
|
||||||
except TemplateNotFound:
|
|
||||||
# re-raise the exception with the correct fileame here.
|
|
||||||
# (the one that includes the prefix)
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
||||||
def list_templates(self):
|
|
||||||
result = []
|
|
||||||
for prefix, loader in self.mapping.iteritems():
|
|
||||||
for template in loader.list_templates():
|
|
||||||
result.append(prefix + self.delimiter + template)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class ChoiceLoader(BaseLoader):
|
|
||||||
"""This loader works like the `PrefixLoader` just that no prefix is
|
|
||||||
specified. If a template could not be found by one loader the next one
|
|
||||||
is tried.
|
|
||||||
|
|
||||||
>>> loader = ChoiceLoader([
|
|
||||||
... FileSystemLoader('/path/to/user/templates'),
|
|
||||||
... FileSystemLoader('/path/to/system/templates')
|
|
||||||
... ])
|
|
||||||
|
|
||||||
This is useful if you want to allow users to override builtin templates
|
|
||||||
from a different location.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, loaders):
|
|
||||||
self.loaders = loaders
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
for loader in self.loaders:
|
|
||||||
try:
|
|
||||||
return loader.get_source(environment, template)
|
|
||||||
except TemplateNotFound:
|
|
||||||
pass
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
||||||
def list_templates(self):
|
|
||||||
found = set()
|
|
||||||
for loader in self.loaders:
|
|
||||||
found.update(loader.list_templates())
|
|
||||||
return sorted(found)
|
|
||||||
|
|
||||||
|
|
||||||
class _TemplateModule(ModuleType):
|
|
||||||
"""Like a normal module but with support for weak references"""
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleLoader(BaseLoader):
|
|
||||||
"""This loader loads templates from precompiled templates.
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
>>> loader = ChoiceLoader([
|
|
||||||
... ModuleLoader('/path/to/compiled/templates'),
|
|
||||||
... FileSystemLoader('/path/to/templates')
|
|
||||||
... ])
|
|
||||||
"""
|
|
||||||
|
|
||||||
has_source_access = False
|
|
||||||
|
|
||||||
def __init__(self, path):
|
|
||||||
package_name = '_jinja2_module_templates_%x' % id(self)
|
|
||||||
|
|
||||||
# create a fake module that looks for the templates in the
|
|
||||||
# path given.
|
|
||||||
mod = _TemplateModule(package_name)
|
|
||||||
if isinstance(path, basestring):
|
|
||||||
path = [path]
|
|
||||||
else:
|
|
||||||
path = list(path)
|
|
||||||
mod.__path__ = path
|
|
||||||
|
|
||||||
sys.modules[package_name] = weakref.proxy(mod,
|
|
||||||
lambda x: sys.modules.pop(package_name, None))
|
|
||||||
|
|
||||||
# the only strong reference, the sys.modules entry is weak
|
|
||||||
# so that the garbage collector can remove it once the
|
|
||||||
# loader that created it goes out of business.
|
|
||||||
self.module = mod
|
|
||||||
self.package_name = package_name
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_template_key(name):
|
|
||||||
return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_module_filename(name):
|
|
||||||
return ModuleLoader.get_template_key(name) + '.py'
|
|
||||||
|
|
||||||
@internalcode
|
|
||||||
def load(self, environment, name, globals=None):
|
|
||||||
key = self.get_template_key(name)
|
|
||||||
module = '%s.%s' % (self.package_name, key)
|
|
||||||
mod = getattr(self.module, module, None)
|
|
||||||
if mod is None:
|
|
||||||
try:
|
|
||||||
mod = __import__(module, None, None, ['root'])
|
|
||||||
except ImportError:
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
|
|
||||||
# remove the entry from sys.modules, we only want the attribute
|
|
||||||
# on the module object we have stored on the loader.
|
|
||||||
sys.modules.pop(module, None)
|
|
||||||
|
|
||||||
return environment.template_class.from_module_dict(
|
|
||||||
environment, mod.__dict__, globals)
|
|
|
@ -1,102 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.meta
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements various functions that exposes information about
|
|
||||||
templates that might be interesting for various kinds of applications.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team, see AUTHORS for more details.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from jinja2 import nodes
|
|
||||||
from jinja2.compiler import CodeGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class TrackingCodeGenerator(CodeGenerator):
|
|
||||||
"""We abuse the code generator for introspection."""
|
|
||||||
|
|
||||||
def __init__(self, environment):
|
|
||||||
CodeGenerator.__init__(self, environment, '<introspection>',
|
|
||||||
'<introspection>')
|
|
||||||
self.undeclared_identifiers = set()
|
|
||||||
|
|
||||||
def write(self, x):
|
|
||||||
"""Don't write."""
|
|
||||||
|
|
||||||
def pull_locals(self, frame):
|
|
||||||
"""Remember all undeclared identifiers."""
|
|
||||||
self.undeclared_identifiers.update(frame.identifiers.undeclared)
|
|
||||||
|
|
||||||
|
|
||||||
def find_undeclared_variables(ast):
|
|
||||||
"""Returns a set of all variables in the AST that will be looked up from
|
|
||||||
the context at runtime. Because at compile time it's not known which
|
|
||||||
variables will be used depending on the path the execution takes at
|
|
||||||
runtime, all variables are returned.
|
|
||||||
|
|
||||||
>>> from jinja2 import Environment, meta
|
|
||||||
>>> env = Environment()
|
|
||||||
>>> ast = env.parse('{% set foo = 42 %}{{ bar + foo }}')
|
|
||||||
>>> meta.find_undeclared_variables(ast)
|
|
||||||
set(['bar'])
|
|
||||||
|
|
||||||
.. admonition:: Implementation
|
|
||||||
|
|
||||||
Internally the code generator is used for finding undeclared variables.
|
|
||||||
This is good to know because the code generator might raise a
|
|
||||||
:exc:`TemplateAssertionError` during compilation and as a matter of
|
|
||||||
fact this function can currently raise that exception as well.
|
|
||||||
"""
|
|
||||||
codegen = TrackingCodeGenerator(ast.environment)
|
|
||||||
codegen.visit(ast)
|
|
||||||
return codegen.undeclared_identifiers
|
|
||||||
|
|
||||||
|
|
||||||
def find_referenced_templates(ast):
|
|
||||||
"""Finds all the referenced templates from the AST. This will return an
|
|
||||||
iterator over all the hardcoded template extensions, inclusions and
|
|
||||||
imports. If dynamic inheritance or inclusion is used, `None` will be
|
|
||||||
yielded.
|
|
||||||
|
|
||||||
>>> from jinja2 import Environment, meta
|
|
||||||
>>> env = Environment()
|
|
||||||
>>> ast = env.parse('{% extends "layout.html" %}{% include helper %}')
|
|
||||||
>>> list(meta.find_referenced_templates(ast))
|
|
||||||
['layout.html', None]
|
|
||||||
|
|
||||||
This function is useful for dependency tracking. For example if you want
|
|
||||||
to rebuild parts of the website after a layout template has changed.
|
|
||||||
"""
|
|
||||||
for node in ast.find_all((nodes.Extends, nodes.FromImport, nodes.Import,
|
|
||||||
nodes.Include)):
|
|
||||||
if not isinstance(node.template, nodes.Const):
|
|
||||||
# a tuple with some non consts in there
|
|
||||||
if isinstance(node.template, (nodes.Tuple, nodes.List)):
|
|
||||||
for template_name in node.template.items:
|
|
||||||
# something const, only yield the strings and ignore
|
|
||||||
# non-string consts that really just make no sense
|
|
||||||
if isinstance(template_name, nodes.Const):
|
|
||||||
if isinstance(template_name.value, basestring):
|
|
||||||
yield template_name.value
|
|
||||||
# something dynamic in there
|
|
||||||
else:
|
|
||||||
yield None
|
|
||||||
# something dynamic we don't know about here
|
|
||||||
else:
|
|
||||||
yield None
|
|
||||||
continue
|
|
||||||
# constant is a basestring, direct template name
|
|
||||||
if isinstance(node.template.value, basestring):
|
|
||||||
yield node.template.value
|
|
||||||
# a tuple or list (latter *should* not happen) made of consts,
|
|
||||||
# yield the consts that are strings. We could warn here for
|
|
||||||
# non string values
|
|
||||||
elif isinstance(node, nodes.Include) and \
|
|
||||||
isinstance(node.template.value, (tuple, list)):
|
|
||||||
for template_name in node.template.value:
|
|
||||||
if isinstance(template_name, basestring):
|
|
||||||
yield template_name
|
|
||||||
# something else we don't care about, we could warn here
|
|
||||||
else:
|
|
||||||
yield None
|
|
|
@ -1,874 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.nodes
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements additional nodes derived from the ast base node.
|
|
||||||
|
|
||||||
It also provides some node tree helper functions like `in_lineno` and
|
|
||||||
`get_nodes` used by the parser and translator in order to normalize
|
|
||||||
python and jinja nodes.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import operator
|
|
||||||
from itertools import chain, izip
|
|
||||||
from collections import deque
|
|
||||||
from jinja2.utils import Markup
|
|
||||||
|
|
||||||
|
|
||||||
_binop_to_func = {
|
|
||||||
'*': operator.mul,
|
|
||||||
'/': operator.truediv,
|
|
||||||
'//': operator.floordiv,
|
|
||||||
'**': operator.pow,
|
|
||||||
'%': operator.mod,
|
|
||||||
'+': operator.add,
|
|
||||||
'-': operator.sub
|
|
||||||
}
|
|
||||||
|
|
||||||
_uaop_to_func = {
|
|
||||||
'not': operator.not_,
|
|
||||||
'+': operator.pos,
|
|
||||||
'-': operator.neg
|
|
||||||
}
|
|
||||||
|
|
||||||
_cmpop_to_func = {
|
|
||||||
'eq': operator.eq,
|
|
||||||
'ne': operator.ne,
|
|
||||||
'gt': operator.gt,
|
|
||||||
'gteq': operator.ge,
|
|
||||||
'lt': operator.lt,
|
|
||||||
'lteq': operator.le,
|
|
||||||
'in': lambda a, b: a in b,
|
|
||||||
'notin': lambda a, b: a not in b
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Impossible(Exception):
|
|
||||||
"""Raised if the node could not perform a requested action."""
|
|
||||||
|
|
||||||
|
|
||||||
class NodeType(type):
|
|
||||||
"""A metaclass for nodes that handles the field and attribute
|
|
||||||
inheritance. fields and attributes from the parent class are
|
|
||||||
automatically forwarded to the child."""
|
|
||||||
|
|
||||||
def __new__(cls, name, bases, d):
|
|
||||||
for attr in 'fields', 'attributes':
|
|
||||||
storage = []
|
|
||||||
storage.extend(getattr(bases[0], attr, ()))
|
|
||||||
storage.extend(d.get(attr, ()))
|
|
||||||
assert len(bases) == 1, 'multiple inheritance not allowed'
|
|
||||||
assert len(storage) == len(set(storage)), 'layout conflict'
|
|
||||||
d[attr] = tuple(storage)
|
|
||||||
d.setdefault('abstract', False)
|
|
||||||
return type.__new__(cls, name, bases, d)
|
|
||||||
|
|
||||||
|
|
||||||
class EvalContext(object):
|
|
||||||
"""Holds evaluation time information. Custom attributes can be attached
|
|
||||||
to it in extensions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, environment, template_name=None):
|
|
||||||
if callable(environment.autoescape):
|
|
||||||
self.autoescape = environment.autoescape(template_name)
|
|
||||||
else:
|
|
||||||
self.autoescape = environment.autoescape
|
|
||||||
self.volatile = False
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
return self.__dict__.copy()
|
|
||||||
|
|
||||||
def revert(self, old):
|
|
||||||
self.__dict__.clear()
|
|
||||||
self.__dict__.update(old)
|
|
||||||
|
|
||||||
|
|
||||||
def get_eval_context(node, ctx):
|
|
||||||
if ctx is None:
|
|
||||||
if node.environment is None:
|
|
||||||
raise RuntimeError('if no eval context is passed, the '
|
|
||||||
'node must have an attached '
|
|
||||||
'environment.')
|
|
||||||
return EvalContext(node.environment)
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
class Node(object):
|
|
||||||
"""Baseclass for all Jinja2 nodes. There are a number of nodes available
|
|
||||||
of different types. There are three major types:
|
|
||||||
|
|
||||||
- :class:`Stmt`: statements
|
|
||||||
- :class:`Expr`: expressions
|
|
||||||
- :class:`Helper`: helper nodes
|
|
||||||
- :class:`Template`: the outermost wrapper node
|
|
||||||
|
|
||||||
All nodes have fields and attributes. Fields may be other nodes, lists,
|
|
||||||
or arbitrary values. Fields are passed to the constructor as regular
|
|
||||||
positional arguments, attributes as keyword arguments. Each node has
|
|
||||||
two attributes: `lineno` (the line number of the node) and `environment`.
|
|
||||||
The `environment` attribute is set at the end of the parsing process for
|
|
||||||
all nodes automatically.
|
|
||||||
"""
|
|
||||||
__metaclass__ = NodeType
|
|
||||||
fields = ()
|
|
||||||
attributes = ('lineno', 'environment')
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def __init__(self, *fields, **attributes):
|
|
||||||
if self.abstract:
|
|
||||||
raise TypeError('abstract nodes are not instanciable')
|
|
||||||
if fields:
|
|
||||||
if len(fields) != len(self.fields):
|
|
||||||
if not self.fields:
|
|
||||||
raise TypeError('%r takes 0 arguments' %
|
|
||||||
self.__class__.__name__)
|
|
||||||
raise TypeError('%r takes 0 or %d argument%s' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
len(self.fields),
|
|
||||||
len(self.fields) != 1 and 's' or ''
|
|
||||||
))
|
|
||||||
for name, arg in izip(self.fields, fields):
|
|
||||||
setattr(self, name, arg)
|
|
||||||
for attr in self.attributes:
|
|
||||||
setattr(self, attr, attributes.pop(attr, None))
|
|
||||||
if attributes:
|
|
||||||
raise TypeError('unknown attribute %r' %
|
|
||||||
iter(attributes).next())
|
|
||||||
|
|
||||||
def iter_fields(self, exclude=None, only=None):
|
|
||||||
"""This method iterates over all fields that are defined and yields
|
|
||||||
``(key, value)`` tuples. Per default all fields are returned, but
|
|
||||||
it's possible to limit that to some fields by providing the `only`
|
|
||||||
parameter or to exclude some using the `exclude` parameter. Both
|
|
||||||
should be sets or tuples of field names.
|
|
||||||
"""
|
|
||||||
for name in self.fields:
|
|
||||||
if (exclude is only is None) or \
|
|
||||||
(exclude is not None and name not in exclude) or \
|
|
||||||
(only is not None and name in only):
|
|
||||||
try:
|
|
||||||
yield name, getattr(self, name)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def iter_child_nodes(self, exclude=None, only=None):
|
|
||||||
"""Iterates over all direct child nodes of the node. This iterates
|
|
||||||
over all fields and yields the values of they are nodes. If the value
|
|
||||||
of a field is a list all the nodes in that list are returned.
|
|
||||||
"""
|
|
||||||
for field, item in self.iter_fields(exclude, only):
|
|
||||||
if isinstance(item, list):
|
|
||||||
for n in item:
|
|
||||||
if isinstance(n, Node):
|
|
||||||
yield n
|
|
||||||
elif isinstance(item, Node):
|
|
||||||
yield item
|
|
||||||
|
|
||||||
def find(self, node_type):
|
|
||||||
"""Find the first node of a given type. If no such node exists the
|
|
||||||
return value is `None`.
|
|
||||||
"""
|
|
||||||
for result in self.find_all(node_type):
|
|
||||||
return result
|
|
||||||
|
|
||||||
def find_all(self, node_type):
|
|
||||||
"""Find all the nodes of a given type. If the type is a tuple,
|
|
||||||
the check is performed for any of the tuple items.
|
|
||||||
"""
|
|
||||||
for child in self.iter_child_nodes():
|
|
||||||
if isinstance(child, node_type):
|
|
||||||
yield child
|
|
||||||
for result in child.find_all(node_type):
|
|
||||||
yield result
|
|
||||||
|
|
||||||
def set_ctx(self, ctx):
|
|
||||||
"""Reset the context of a node and all child nodes. Per default the
|
|
||||||
parser will all generate nodes that have a 'load' context as it's the
|
|
||||||
most common one. This method is used in the parser to set assignment
|
|
||||||
targets and other nodes to a store context.
|
|
||||||
"""
|
|
||||||
todo = deque([self])
|
|
||||||
while todo:
|
|
||||||
node = todo.popleft()
|
|
||||||
if 'ctx' in node.fields:
|
|
||||||
node.ctx = ctx
|
|
||||||
todo.extend(node.iter_child_nodes())
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_lineno(self, lineno, override=False):
|
|
||||||
"""Set the line numbers of the node and children."""
|
|
||||||
todo = deque([self])
|
|
||||||
while todo:
|
|
||||||
node = todo.popleft()
|
|
||||||
if 'lineno' in node.attributes:
|
|
||||||
if node.lineno is None or override:
|
|
||||||
node.lineno = lineno
|
|
||||||
todo.extend(node.iter_child_nodes())
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_environment(self, environment):
|
|
||||||
"""Set the environment for all nodes."""
|
|
||||||
todo = deque([self])
|
|
||||||
while todo:
|
|
||||||
node = todo.popleft()
|
|
||||||
node.environment = environment
|
|
||||||
todo.extend(node.iter_child_nodes())
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return type(self) is type(other) and \
|
|
||||||
tuple(self.iter_fields()) == tuple(other.iter_fields())
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s(%s)' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
', '.join('%s=%r' % (arg, getattr(self, arg, None)) for
|
|
||||||
arg in self.fields)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Stmt(Node):
|
|
||||||
"""Base node for all statements."""
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Helper(Node):
|
|
||||||
"""Nodes that exist in a specific context only."""
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Template(Node):
|
|
||||||
"""Node that represents a template. This must be the outermost node that
|
|
||||||
is passed to the compiler.
|
|
||||||
"""
|
|
||||||
fields = ('body',)
|
|
||||||
|
|
||||||
|
|
||||||
class Output(Stmt):
|
|
||||||
"""A node that holds multiple expressions which are then printed out.
|
|
||||||
This is used both for the `print` statement and the regular template data.
|
|
||||||
"""
|
|
||||||
fields = ('nodes',)
|
|
||||||
|
|
||||||
|
|
||||||
class Extends(Stmt):
|
|
||||||
"""Represents an extends statement."""
|
|
||||||
fields = ('template',)
|
|
||||||
|
|
||||||
|
|
||||||
class For(Stmt):
|
|
||||||
"""The for loop. `target` is the target for the iteration (usually a
|
|
||||||
:class:`Name` or :class:`Tuple`), `iter` the iterable. `body` is a list
|
|
||||||
of nodes that are used as loop-body, and `else_` a list of nodes for the
|
|
||||||
`else` block. If no else node exists it has to be an empty list.
|
|
||||||
|
|
||||||
For filtered nodes an expression can be stored as `test`, otherwise `None`.
|
|
||||||
"""
|
|
||||||
fields = ('target', 'iter', 'body', 'else_', 'test', 'recursive')
|
|
||||||
|
|
||||||
|
|
||||||
class If(Stmt):
|
|
||||||
"""If `test` is true, `body` is rendered, else `else_`."""
|
|
||||||
fields = ('test', 'body', 'else_')
|
|
||||||
|
|
||||||
|
|
||||||
class Macro(Stmt):
|
|
||||||
"""A macro definition. `name` is the name of the macro, `args` a list of
|
|
||||||
arguments and `defaults` a list of defaults if there are any. `body` is
|
|
||||||
a list of nodes for the macro body.
|
|
||||||
"""
|
|
||||||
fields = ('name', 'args', 'defaults', 'body')
|
|
||||||
|
|
||||||
|
|
||||||
class CallBlock(Stmt):
|
|
||||||
"""Like a macro without a name but a call instead. `call` is called with
|
|
||||||
the unnamed macro as `caller` argument this node holds.
|
|
||||||
"""
|
|
||||||
fields = ('call', 'args', 'defaults', 'body')
|
|
||||||
|
|
||||||
|
|
||||||
class FilterBlock(Stmt):
|
|
||||||
"""Node for filter sections."""
|
|
||||||
fields = ('body', 'filter')
|
|
||||||
|
|
||||||
|
|
||||||
class Block(Stmt):
|
|
||||||
"""A node that represents a block."""
|
|
||||||
fields = ('name', 'body', 'scoped')
|
|
||||||
|
|
||||||
|
|
||||||
class Include(Stmt):
|
|
||||||
"""A node that represents the include tag."""
|
|
||||||
fields = ('template', 'with_context', 'ignore_missing')
|
|
||||||
|
|
||||||
|
|
||||||
class Import(Stmt):
|
|
||||||
"""A node that represents the import tag."""
|
|
||||||
fields = ('template', 'target', 'with_context')
|
|
||||||
|
|
||||||
|
|
||||||
class FromImport(Stmt):
|
|
||||||
"""A node that represents the from import tag. It's important to not
|
|
||||||
pass unsafe names to the name attribute. The compiler translates the
|
|
||||||
attribute lookups directly into getattr calls and does *not* use the
|
|
||||||
subscript callback of the interface. As exported variables may not
|
|
||||||
start with double underscores (which the parser asserts) this is not a
|
|
||||||
problem for regular Jinja code, but if this node is used in an extension
|
|
||||||
extra care must be taken.
|
|
||||||
|
|
||||||
The list of names may contain tuples if aliases are wanted.
|
|
||||||
"""
|
|
||||||
fields = ('template', 'names', 'with_context')
|
|
||||||
|
|
||||||
|
|
||||||
class ExprStmt(Stmt):
|
|
||||||
"""A statement that evaluates an expression and discards the result."""
|
|
||||||
fields = ('node',)
|
|
||||||
|
|
||||||
|
|
||||||
class Assign(Stmt):
|
|
||||||
"""Assigns an expression to a target."""
|
|
||||||
fields = ('target', 'node')
|
|
||||||
|
|
||||||
|
|
||||||
class Expr(Node):
|
|
||||||
"""Baseclass for all expressions."""
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
"""Return the value of the expression as constant or raise
|
|
||||||
:exc:`Impossible` if this was not possible.
|
|
||||||
|
|
||||||
An :class:`EvalContext` can be provided, if none is given
|
|
||||||
a default context is created which requires the nodes to have
|
|
||||||
an attached environment.
|
|
||||||
|
|
||||||
.. versionchanged:: 2.4
|
|
||||||
the `eval_ctx` parameter was added.
|
|
||||||
"""
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
def can_assign(self):
|
|
||||||
"""Check if it's possible to assign something to this node."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class BinExpr(Expr):
|
|
||||||
"""Baseclass for all binary expressions."""
|
|
||||||
fields = ('left', 'right')
|
|
||||||
operator = None
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
f = _binop_to_func[self.operator]
|
|
||||||
try:
|
|
||||||
return f(self.left.as_const(eval_ctx), self.right.as_const(eval_ctx))
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
|
|
||||||
class UnaryExpr(Expr):
|
|
||||||
"""Baseclass for all unary expressions."""
|
|
||||||
fields = ('node',)
|
|
||||||
operator = None
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
f = _uaop_to_func[self.operator]
|
|
||||||
try:
|
|
||||||
return f(self.node.as_const(eval_ctx))
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
|
|
||||||
class Name(Expr):
|
|
||||||
"""Looks up a name or stores a value in a name.
|
|
||||||
The `ctx` of the node can be one of the following values:
|
|
||||||
|
|
||||||
- `store`: store a value in the name
|
|
||||||
- `load`: load that name
|
|
||||||
- `param`: like `store` but if the name was defined as function parameter.
|
|
||||||
"""
|
|
||||||
fields = ('name', 'ctx')
|
|
||||||
|
|
||||||
def can_assign(self):
|
|
||||||
return self.name not in ('true', 'false', 'none',
|
|
||||||
'True', 'False', 'None')
|
|
||||||
|
|
||||||
|
|
||||||
class Literal(Expr):
|
|
||||||
"""Baseclass for literals."""
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Const(Literal):
|
|
||||||
"""All constant values. The parser will return this node for simple
|
|
||||||
constants such as ``42`` or ``"foo"`` but it can be used to store more
|
|
||||||
complex values such as lists too. Only constants with a safe
|
|
||||||
representation (objects where ``eval(repr(x)) == x`` is true).
|
|
||||||
"""
|
|
||||||
fields = ('value',)
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_untrusted(cls, value, lineno=None, environment=None):
|
|
||||||
"""Return a const object if the value is representable as
|
|
||||||
constant value in the generated code, otherwise it will raise
|
|
||||||
an `Impossible` exception.
|
|
||||||
"""
|
|
||||||
from compiler import has_safe_repr
|
|
||||||
if not has_safe_repr(value):
|
|
||||||
raise Impossible()
|
|
||||||
return cls(value, lineno=lineno, environment=environment)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateData(Literal):
|
|
||||||
"""A constant template string."""
|
|
||||||
fields = ('data',)
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
if get_eval_context(self, eval_ctx).autoescape:
|
|
||||||
return Markup(self.data)
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
|
|
||||||
class Tuple(Literal):
|
|
||||||
"""For loop unpacking and some other things like multiple arguments
|
|
||||||
for subscripts. Like for :class:`Name` `ctx` specifies if the tuple
|
|
||||||
is used for loading the names or storing.
|
|
||||||
"""
|
|
||||||
fields = ('items', 'ctx')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return tuple(x.as_const(eval_ctx) for x in self.items)
|
|
||||||
|
|
||||||
def can_assign(self):
|
|
||||||
for item in self.items:
|
|
||||||
if not item.can_assign():
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class List(Literal):
|
|
||||||
"""Any list literal such as ``[1, 2, 3]``"""
|
|
||||||
fields = ('items',)
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return [x.as_const(eval_ctx) for x in self.items]
|
|
||||||
|
|
||||||
|
|
||||||
class Dict(Literal):
|
|
||||||
"""Any dict literal such as ``{1: 2, 3: 4}``. The items must be a list of
|
|
||||||
:class:`Pair` nodes.
|
|
||||||
"""
|
|
||||||
fields = ('items',)
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return dict(x.as_const(eval_ctx) for x in self.items)
|
|
||||||
|
|
||||||
|
|
||||||
class Pair(Helper):
|
|
||||||
"""A key, value pair for dicts."""
|
|
||||||
fields = ('key', 'value')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return self.key.as_const(eval_ctx), self.value.as_const(eval_ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class Keyword(Helper):
|
|
||||||
"""A key, value pair for keyword arguments where key is a string."""
|
|
||||||
fields = ('key', 'value')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return self.key, self.value.as_const(eval_ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class CondExpr(Expr):
|
|
||||||
"""A conditional expression (inline if expression). (``{{
|
|
||||||
foo if bar else baz }}``)
|
|
||||||
"""
|
|
||||||
fields = ('test', 'expr1', 'expr2')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
if self.test.as_const(eval_ctx):
|
|
||||||
return self.expr1.as_const(eval_ctx)
|
|
||||||
|
|
||||||
# if we evaluate to an undefined object, we better do that at runtime
|
|
||||||
if self.expr2 is None:
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
return self.expr2.as_const(eval_ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class Filter(Expr):
|
|
||||||
"""This node applies a filter on an expression. `name` is the name of
|
|
||||||
the filter, the rest of the fields are the same as for :class:`Call`.
|
|
||||||
|
|
||||||
If the `node` of a filter is `None` the contents of the last buffer are
|
|
||||||
filtered. Buffers are created by macros and filter blocks.
|
|
||||||
"""
|
|
||||||
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
if eval_ctx.volatile or self.node is None:
|
|
||||||
raise Impossible()
|
|
||||||
# we have to be careful here because we call filter_ below.
|
|
||||||
# if this variable would be called filter, 2to3 would wrap the
|
|
||||||
# call in a list beause it is assuming we are talking about the
|
|
||||||
# builtin filter function here which no longer returns a list in
|
|
||||||
# python 3. because of that, do not rename filter_ to filter!
|
|
||||||
filter_ = self.environment.filters.get(self.name)
|
|
||||||
if filter_ is None or getattr(filter_, 'contextfilter', False):
|
|
||||||
raise Impossible()
|
|
||||||
obj = self.node.as_const(eval_ctx)
|
|
||||||
args = [x.as_const(eval_ctx) for x in self.args]
|
|
||||||
if getattr(filter_, 'evalcontextfilter', False):
|
|
||||||
args.insert(0, eval_ctx)
|
|
||||||
elif getattr(filter_, 'environmentfilter', False):
|
|
||||||
args.insert(0, self.environment)
|
|
||||||
kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs)
|
|
||||||
if self.dyn_args is not None:
|
|
||||||
try:
|
|
||||||
args.extend(self.dyn_args.as_const(eval_ctx))
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
if self.dyn_kwargs is not None:
|
|
||||||
try:
|
|
||||||
kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
try:
|
|
||||||
return filter_(obj, *args, **kwargs)
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
|
|
||||||
class Test(Expr):
|
|
||||||
"""Applies a test on an expression. `name` is the name of the test, the
|
|
||||||
rest of the fields are the same as for :class:`Call`.
|
|
||||||
"""
|
|
||||||
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
|
|
||||||
|
|
||||||
|
|
||||||
class Call(Expr):
|
|
||||||
"""Calls an expression. `args` is a list of arguments, `kwargs` a list
|
|
||||||
of keyword arguments (list of :class:`Keyword` nodes), and `dyn_args`
|
|
||||||
and `dyn_kwargs` has to be either `None` or a node that is used as
|
|
||||||
node for dynamic positional (``*args``) or keyword (``**kwargs``)
|
|
||||||
arguments.
|
|
||||||
"""
|
|
||||||
fields = ('node', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
if eval_ctx.volatile:
|
|
||||||
raise Impossible()
|
|
||||||
obj = self.node.as_const(eval_ctx)
|
|
||||||
|
|
||||||
# don't evaluate context functions
|
|
||||||
args = [x.as_const(eval_ctx) for x in self.args]
|
|
||||||
if getattr(obj, 'contextfunction', False):
|
|
||||||
raise Impossible()
|
|
||||||
elif getattr(obj, 'evalcontextfunction', False):
|
|
||||||
args.insert(0, eval_ctx)
|
|
||||||
elif getattr(obj, 'environmentfunction', False):
|
|
||||||
args.insert(0, self.environment)
|
|
||||||
|
|
||||||
kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs)
|
|
||||||
if self.dyn_args is not None:
|
|
||||||
try:
|
|
||||||
args.extend(self.dyn_args.as_const(eval_ctx))
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
if self.dyn_kwargs is not None:
|
|
||||||
try:
|
|
||||||
kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
try:
|
|
||||||
return obj(*args, **kwargs)
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
|
|
||||||
class Getitem(Expr):
|
|
||||||
"""Get an attribute or item from an expression and prefer the item."""
|
|
||||||
fields = ('node', 'arg', 'ctx')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
if self.ctx != 'load':
|
|
||||||
raise Impossible()
|
|
||||||
try:
|
|
||||||
return self.environment.getitem(self.node.as_const(eval_ctx),
|
|
||||||
self.arg.as_const(eval_ctx))
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
def can_assign(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Getattr(Expr):
|
|
||||||
"""Get an attribute or item from an expression that is a ascii-only
|
|
||||||
bytestring and prefer the attribute.
|
|
||||||
"""
|
|
||||||
fields = ('node', 'attr', 'ctx')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
if self.ctx != 'load':
|
|
||||||
raise Impossible()
|
|
||||||
try:
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return self.environment.getattr(self.node.as_const(eval_ctx), arg)
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
|
|
||||||
def can_assign(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Slice(Expr):
|
|
||||||
"""Represents a slice object. This must only be used as argument for
|
|
||||||
:class:`Subscript`.
|
|
||||||
"""
|
|
||||||
fields = ('start', 'stop', 'step')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
def const(obj):
|
|
||||||
if obj is None:
|
|
||||||
return None
|
|
||||||
return obj.as_const(eval_ctx)
|
|
||||||
return slice(const(self.start), const(self.stop), const(self.step))
|
|
||||||
|
|
||||||
|
|
||||||
class Concat(Expr):
|
|
||||||
"""Concatenates the list of expressions provided after converting them to
|
|
||||||
unicode.
|
|
||||||
"""
|
|
||||||
fields = ('nodes',)
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return ''.join(unicode(x.as_const(eval_ctx)) for x in self.nodes)
|
|
||||||
|
|
||||||
|
|
||||||
class Compare(Expr):
|
|
||||||
"""Compares an expression with some other expressions. `ops` must be a
|
|
||||||
list of :class:`Operand`\s.
|
|
||||||
"""
|
|
||||||
fields = ('expr', 'ops')
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
result = value = self.expr.as_const(eval_ctx)
|
|
||||||
try:
|
|
||||||
for op in self.ops:
|
|
||||||
new_value = op.expr.as_const(eval_ctx)
|
|
||||||
result = _cmpop_to_func[op.op](value, new_value)
|
|
||||||
value = new_value
|
|
||||||
except:
|
|
||||||
raise Impossible()
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class Operand(Helper):
|
|
||||||
"""Holds an operator and an expression."""
|
|
||||||
fields = ('op', 'expr')
|
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
Operand.__doc__ += '\nThe following operators are available: ' + \
|
|
||||||
', '.join(sorted('``%s``' % x for x in set(_binop_to_func) |
|
|
||||||
set(_uaop_to_func) | set(_cmpop_to_func)))
|
|
||||||
|
|
||||||
|
|
||||||
class Mul(BinExpr):
|
|
||||||
"""Multiplies the left with the right node."""
|
|
||||||
operator = '*'
|
|
||||||
|
|
||||||
|
|
||||||
class Div(BinExpr):
|
|
||||||
"""Divides the left by the right node."""
|
|
||||||
operator = '/'
|
|
||||||
|
|
||||||
|
|
||||||
class FloorDiv(BinExpr):
|
|
||||||
"""Divides the left by the right node and truncates conver the
|
|
||||||
result into an integer by truncating.
|
|
||||||
"""
|
|
||||||
operator = '//'
|
|
||||||
|
|
||||||
|
|
||||||
class Add(BinExpr):
|
|
||||||
"""Add the left to the right node."""
|
|
||||||
operator = '+'
|
|
||||||
|
|
||||||
|
|
||||||
class Sub(BinExpr):
|
|
||||||
"""Substract the right from the left node."""
|
|
||||||
operator = '-'
|
|
||||||
|
|
||||||
|
|
||||||
class Mod(BinExpr):
|
|
||||||
"""Left modulo right."""
|
|
||||||
operator = '%'
|
|
||||||
|
|
||||||
|
|
||||||
class Pow(BinExpr):
|
|
||||||
"""Left to the power of right."""
|
|
||||||
operator = '**'
|
|
||||||
|
|
||||||
|
|
||||||
class And(BinExpr):
|
|
||||||
"""Short circuited AND."""
|
|
||||||
operator = 'and'
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return self.left.as_const(eval_ctx) and self.right.as_const(eval_ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class Or(BinExpr):
|
|
||||||
"""Short circuited OR."""
|
|
||||||
operator = 'or'
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return self.left.as_const(eval_ctx) or self.right.as_const(eval_ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class Not(UnaryExpr):
|
|
||||||
"""Negate the expression."""
|
|
||||||
operator = 'not'
|
|
||||||
|
|
||||||
|
|
||||||
class Neg(UnaryExpr):
|
|
||||||
"""Make the expression negative."""
|
|
||||||
operator = '-'
|
|
||||||
|
|
||||||
|
|
||||||
class Pos(UnaryExpr):
|
|
||||||
"""Make the expression positive (noop for most expressions)"""
|
|
||||||
operator = '+'
|
|
||||||
|
|
||||||
|
|
||||||
# Helpers for extensions
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentAttribute(Expr):
|
|
||||||
"""Loads an attribute from the environment object. This is useful for
|
|
||||||
extensions that want to call a callback stored on the environment.
|
|
||||||
"""
|
|
||||||
fields = ('name',)
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionAttribute(Expr):
|
|
||||||
"""Returns the attribute of an extension bound to the environment.
|
|
||||||
The identifier is the identifier of the :class:`Extension`.
|
|
||||||
|
|
||||||
This node is usually constructed by calling the
|
|
||||||
:meth:`~jinja2.ext.Extension.attr` method on an extension.
|
|
||||||
"""
|
|
||||||
fields = ('identifier', 'name')
|
|
||||||
|
|
||||||
|
|
||||||
class ImportedName(Expr):
|
|
||||||
"""If created with an import name the import name is returned on node
|
|
||||||
access. For example ``ImportedName('cgi.escape')`` returns the `escape`
|
|
||||||
function from the cgi module on evaluation. Imports are optimized by the
|
|
||||||
compiler so there is no need to assign them to local variables.
|
|
||||||
"""
|
|
||||||
fields = ('importname',)
|
|
||||||
|
|
||||||
|
|
||||||
class InternalName(Expr):
|
|
||||||
"""An internal name in the compiler. You cannot create these nodes
|
|
||||||
yourself but the parser provides a
|
|
||||||
:meth:`~jinja2.parser.Parser.free_identifier` method that creates
|
|
||||||
a new identifier for you. This identifier is not available from the
|
|
||||||
template and is not threated specially by the compiler.
|
|
||||||
"""
|
|
||||||
fields = ('name',)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
raise TypeError('Can\'t create internal names. Use the '
|
|
||||||
'`free_identifier` method on a parser.')
|
|
||||||
|
|
||||||
|
|
||||||
class MarkSafe(Expr):
|
|
||||||
"""Mark the wrapped expression as safe (wrap it as `Markup`)."""
|
|
||||||
fields = ('expr',)
|
|
||||||
|
|
||||||
def as_const(self, eval_ctx=None):
|
|
||||||
eval_ctx = get_eval_context(self, eval_ctx)
|
|
||||||
return Markup(self.expr.as_const(eval_ctx))
|
|
||||||
|
|
||||||
|
|
||||||
class ContextReference(Expr):
|
|
||||||
"""Returns the current template context. It can be used like a
|
|
||||||
:class:`Name` node, with a ``'load'`` ctx and will return the
|
|
||||||
current :class:`~jinja2.runtime.Context` object.
|
|
||||||
|
|
||||||
Here an example that assigns the current template name to a
|
|
||||||
variable named `foo`::
|
|
||||||
|
|
||||||
Assign(Name('foo', ctx='store'),
|
|
||||||
Getattr(ContextReference(), 'name'))
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Continue(Stmt):
|
|
||||||
"""Continue a loop."""
|
|
||||||
|
|
||||||
|
|
||||||
class Break(Stmt):
|
|
||||||
"""Break a loop."""
|
|
||||||
|
|
||||||
|
|
||||||
class Scope(Stmt):
|
|
||||||
"""An artificial scope."""
|
|
||||||
fields = ('body',)
|
|
||||||
|
|
||||||
|
|
||||||
class EvalContextModifier(Stmt):
|
|
||||||
"""Modifies the eval context. For each option that should be modified,
|
|
||||||
a :class:`Keyword` has to be added to the :attr:`options` list.
|
|
||||||
|
|
||||||
Example to change the `autoescape` setting::
|
|
||||||
|
|
||||||
EvalContextModifier(options=[Keyword('autoescape', Const(True))])
|
|
||||||
"""
|
|
||||||
fields = ('options',)
|
|
||||||
|
|
||||||
|
|
||||||
class ScopedEvalContextModifier(EvalContextModifier):
|
|
||||||
"""Modifies the eval context and reverts it later. Works exactly like
|
|
||||||
:class:`EvalContextModifier` but will only modify the
|
|
||||||
:class:`~jinja2.nodes.EvalContext` for nodes in the :attr:`body`.
|
|
||||||
"""
|
|
||||||
fields = ('body',)
|
|
||||||
|
|
||||||
|
|
||||||
# make sure nobody creates custom nodes
|
|
||||||
def _failing_new(*args, **kwargs):
|
|
||||||
raise TypeError('can\'t create custom node types')
|
|
||||||
NodeType.__new__ = staticmethod(_failing_new); del _failing_new
|
|
|
@ -1,68 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.optimizer
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The jinja optimizer is currently trying to constant fold a few expressions
|
|
||||||
and modify the AST in place so that it should be easier to evaluate it.
|
|
||||||
|
|
||||||
Because the AST does not contain all the scoping information and the
|
|
||||||
compiler has to find that out, we cannot do all the optimizations we
|
|
||||||
want. For example loop unrolling doesn't work because unrolled loops would
|
|
||||||
have a different scoping.
|
|
||||||
|
|
||||||
The solution would be a second syntax tree that has the scoping rules stored.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD.
|
|
||||||
"""
|
|
||||||
from jinja2 import nodes
|
|
||||||
from jinja2.visitor import NodeTransformer
|
|
||||||
|
|
||||||
|
|
||||||
def optimize(node, environment):
|
|
||||||
"""The context hint can be used to perform an static optimization
|
|
||||||
based on the context given."""
|
|
||||||
optimizer = Optimizer(environment)
|
|
||||||
return optimizer.visit(node)
|
|
||||||
|
|
||||||
|
|
||||||
class Optimizer(NodeTransformer):
|
|
||||||
|
|
||||||
def __init__(self, environment):
|
|
||||||
self.environment = environment
|
|
||||||
|
|
||||||
def visit_If(self, node):
|
|
||||||
"""Eliminate dead code."""
|
|
||||||
# do not optimize ifs that have a block inside so that it doesn't
|
|
||||||
# break super().
|
|
||||||
if node.find(nodes.Block) is not None:
|
|
||||||
return self.generic_visit(node)
|
|
||||||
try:
|
|
||||||
val = self.visit(node.test).as_const()
|
|
||||||
except nodes.Impossible:
|
|
||||||
return self.generic_visit(node)
|
|
||||||
if val:
|
|
||||||
body = node.body
|
|
||||||
else:
|
|
||||||
body = node.else_
|
|
||||||
result = []
|
|
||||||
for node in body:
|
|
||||||
result.extend(self.visit_list(node))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def fold(self, node):
|
|
||||||
"""Do constant folding."""
|
|
||||||
node = self.generic_visit(node)
|
|
||||||
try:
|
|
||||||
return nodes.Const.from_untrusted(node.as_const(),
|
|
||||||
lineno=node.lineno,
|
|
||||||
environment=self.environment)
|
|
||||||
except nodes.Impossible:
|
|
||||||
return node
|
|
||||||
|
|
||||||
visit_Add = visit_Sub = visit_Mul = visit_Div = visit_FloorDiv = \
|
|
||||||
visit_Pow = visit_Mod = visit_And = visit_Or = visit_Pos = visit_Neg = \
|
|
||||||
visit_Not = visit_Compare = visit_Getitem = visit_Getattr = visit_Call = \
|
|
||||||
visit_Filter = visit_Test = visit_CondExpr = fold
|
|
||||||
del fold
|
|
|
@ -1,882 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.parser
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements the template parser.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from jinja2 import nodes
|
|
||||||
from jinja2.exceptions import TemplateSyntaxError, TemplateAssertionError
|
|
||||||
from jinja2.utils import next
|
|
||||||
from jinja2.lexer import describe_token, describe_token_expr
|
|
||||||
|
|
||||||
|
|
||||||
#: statements that callinto
|
|
||||||
_statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print',
|
|
||||||
'macro', 'include', 'from', 'import',
|
|
||||||
'set'])
|
|
||||||
_compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq'])
|
|
||||||
|
|
||||||
|
|
||||||
class Parser(object):
|
|
||||||
"""This is the central parsing class Jinja2 uses. It's passed to
|
|
||||||
extensions and can be used to parse expressions or statements.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, environment, source, name=None, filename=None,
|
|
||||||
state=None):
|
|
||||||
self.environment = environment
|
|
||||||
self.stream = environment._tokenize(source, name, filename, state)
|
|
||||||
self.name = name
|
|
||||||
self.filename = filename
|
|
||||||
self.closed = False
|
|
||||||
self.extensions = {}
|
|
||||||
for extension in environment.iter_extensions():
|
|
||||||
for tag in extension.tags:
|
|
||||||
self.extensions[tag] = extension.parse
|
|
||||||
self._last_identifier = 0
|
|
||||||
self._tag_stack = []
|
|
||||||
self._end_token_stack = []
|
|
||||||
|
|
||||||
def fail(self, msg, lineno=None, exc=TemplateSyntaxError):
|
|
||||||
"""Convenience method that raises `exc` with the message, passed
|
|
||||||
line number or last line number as well as the current name and
|
|
||||||
filename.
|
|
||||||
"""
|
|
||||||
if lineno is None:
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
raise exc(msg, lineno, self.name, self.filename)
|
|
||||||
|
|
||||||
def _fail_ut_eof(self, name, end_token_stack, lineno):
|
|
||||||
expected = []
|
|
||||||
for exprs in end_token_stack:
|
|
||||||
expected.extend(map(describe_token_expr, exprs))
|
|
||||||
if end_token_stack:
|
|
||||||
currently_looking = ' or '.join(
|
|
||||||
"'%s'" % describe_token_expr(expr)
|
|
||||||
for expr in end_token_stack[-1])
|
|
||||||
else:
|
|
||||||
currently_looking = None
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
message = ['Unexpected end of template.']
|
|
||||||
else:
|
|
||||||
message = ['Encountered unknown tag \'%s\'.' % name]
|
|
||||||
|
|
||||||
if currently_looking:
|
|
||||||
if name is not None and name in expected:
|
|
||||||
message.append('You probably made a nesting mistake. Jinja '
|
|
||||||
'is expecting this tag, but currently looking '
|
|
||||||
'for %s.' % currently_looking)
|
|
||||||
else:
|
|
||||||
message.append('Jinja was looking for the following tags: '
|
|
||||||
'%s.' % currently_looking)
|
|
||||||
|
|
||||||
if self._tag_stack:
|
|
||||||
message.append('The innermost block that needs to be '
|
|
||||||
'closed is \'%s\'.' % self._tag_stack[-1])
|
|
||||||
|
|
||||||
self.fail(' '.join(message), lineno)
|
|
||||||
|
|
||||||
def fail_unknown_tag(self, name, lineno=None):
|
|
||||||
"""Called if the parser encounters an unknown tag. Tries to fail
|
|
||||||
with a human readable error message that could help to identify
|
|
||||||
the problem.
|
|
||||||
"""
|
|
||||||
return self._fail_ut_eof(name, self._end_token_stack, lineno)
|
|
||||||
|
|
||||||
def fail_eof(self, end_tokens=None, lineno=None):
|
|
||||||
"""Like fail_unknown_tag but for end of template situations."""
|
|
||||||
stack = list(self._end_token_stack)
|
|
||||||
if end_tokens is not None:
|
|
||||||
stack.append(end_tokens)
|
|
||||||
return self._fail_ut_eof(None, stack, lineno)
|
|
||||||
|
|
||||||
def is_tuple_end(self, extra_end_rules=None):
|
|
||||||
"""Are we at the end of a tuple?"""
|
|
||||||
if self.stream.current.type in ('variable_end', 'block_end', 'rparen'):
|
|
||||||
return True
|
|
||||||
elif extra_end_rules is not None:
|
|
||||||
return self.stream.current.test_any(extra_end_rules)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def free_identifier(self, lineno=None):
|
|
||||||
"""Return a new free identifier as :class:`~jinja2.nodes.InternalName`."""
|
|
||||||
self._last_identifier += 1
|
|
||||||
rv = object.__new__(nodes.InternalName)
|
|
||||||
nodes.Node.__init__(rv, 'fi%d' % self._last_identifier, lineno=lineno)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def parse_statement(self):
|
|
||||||
"""Parse a single statement."""
|
|
||||||
token = self.stream.current
|
|
||||||
if token.type != 'name':
|
|
||||||
self.fail('tag name expected', token.lineno)
|
|
||||||
self._tag_stack.append(token.value)
|
|
||||||
pop_tag = True
|
|
||||||
try:
|
|
||||||
if token.value in _statement_keywords:
|
|
||||||
return getattr(self, 'parse_' + self.stream.current.value)()
|
|
||||||
if token.value == 'call':
|
|
||||||
return self.parse_call_block()
|
|
||||||
if token.value == 'filter':
|
|
||||||
return self.parse_filter_block()
|
|
||||||
ext = self.extensions.get(token.value)
|
|
||||||
if ext is not None:
|
|
||||||
return ext(self)
|
|
||||||
|
|
||||||
# did not work out, remove the token we pushed by accident
|
|
||||||
# from the stack so that the unknown tag fail function can
|
|
||||||
# produce a proper error message.
|
|
||||||
self._tag_stack.pop()
|
|
||||||
pop_tag = False
|
|
||||||
self.fail_unknown_tag(token.value, token.lineno)
|
|
||||||
finally:
|
|
||||||
if pop_tag:
|
|
||||||
self._tag_stack.pop()
|
|
||||||
|
|
||||||
def parse_statements(self, end_tokens, drop_needle=False):
|
|
||||||
"""Parse multiple statements into a list until one of the end tokens
|
|
||||||
is reached. This is used to parse the body of statements as it also
|
|
||||||
parses template data if appropriate. The parser checks first if the
|
|
||||||
current token is a colon and skips it if there is one. Then it checks
|
|
||||||
for the block end and parses until if one of the `end_tokens` is
|
|
||||||
reached. Per default the active token in the stream at the end of
|
|
||||||
the call is the matched end token. If this is not wanted `drop_needle`
|
|
||||||
can be set to `True` and the end token is removed.
|
|
||||||
"""
|
|
||||||
# the first token may be a colon for python compatibility
|
|
||||||
self.stream.skip_if('colon')
|
|
||||||
|
|
||||||
# in the future it would be possible to add whole code sections
|
|
||||||
# by adding some sort of end of statement token and parsing those here.
|
|
||||||
self.stream.expect('block_end')
|
|
||||||
result = self.subparse(end_tokens)
|
|
||||||
|
|
||||||
# we reached the end of the template too early, the subparser
|
|
||||||
# does not check for this, so we do that now
|
|
||||||
if self.stream.current.type == 'eof':
|
|
||||||
self.fail_eof(end_tokens)
|
|
||||||
|
|
||||||
if drop_needle:
|
|
||||||
next(self.stream)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def parse_set(self):
|
|
||||||
"""Parse an assign statement."""
|
|
||||||
lineno = next(self.stream).lineno
|
|
||||||
target = self.parse_assign_target()
|
|
||||||
self.stream.expect('assign')
|
|
||||||
expr = self.parse_tuple()
|
|
||||||
return nodes.Assign(target, expr, lineno=lineno)
|
|
||||||
|
|
||||||
def parse_for(self):
|
|
||||||
"""Parse a for loop."""
|
|
||||||
lineno = self.stream.expect('name:for').lineno
|
|
||||||
target = self.parse_assign_target(extra_end_rules=('name:in',))
|
|
||||||
self.stream.expect('name:in')
|
|
||||||
iter = self.parse_tuple(with_condexpr=False,
|
|
||||||
extra_end_rules=('name:recursive',))
|
|
||||||
test = None
|
|
||||||
if self.stream.skip_if('name:if'):
|
|
||||||
test = self.parse_expression()
|
|
||||||
recursive = self.stream.skip_if('name:recursive')
|
|
||||||
body = self.parse_statements(('name:endfor', 'name:else'))
|
|
||||||
if next(self.stream).value == 'endfor':
|
|
||||||
else_ = []
|
|
||||||
else:
|
|
||||||
else_ = self.parse_statements(('name:endfor',), drop_needle=True)
|
|
||||||
return nodes.For(target, iter, body, else_, test,
|
|
||||||
recursive, lineno=lineno)
|
|
||||||
|
|
||||||
def parse_if(self):
|
|
||||||
"""Parse an if construct."""
|
|
||||||
node = result = nodes.If(lineno=self.stream.expect('name:if').lineno)
|
|
||||||
while 1:
|
|
||||||
node.test = self.parse_tuple(with_condexpr=False)
|
|
||||||
node.body = self.parse_statements(('name:elif', 'name:else',
|
|
||||||
'name:endif'))
|
|
||||||
token = next(self.stream)
|
|
||||||
if token.test('name:elif'):
|
|
||||||
new_node = nodes.If(lineno=self.stream.current.lineno)
|
|
||||||
node.else_ = [new_node]
|
|
||||||
node = new_node
|
|
||||||
continue
|
|
||||||
elif token.test('name:else'):
|
|
||||||
node.else_ = self.parse_statements(('name:endif',),
|
|
||||||
drop_needle=True)
|
|
||||||
else:
|
|
||||||
node.else_ = []
|
|
||||||
break
|
|
||||||
return result
|
|
||||||
|
|
||||||
def parse_block(self):
|
|
||||||
node = nodes.Block(lineno=next(self.stream).lineno)
|
|
||||||
node.name = self.stream.expect('name').value
|
|
||||||
node.scoped = self.stream.skip_if('name:scoped')
|
|
||||||
|
|
||||||
# common problem people encounter when switching from django
|
|
||||||
# to jinja. we do not support hyphens in block names, so let's
|
|
||||||
# raise a nicer error message in that case.
|
|
||||||
if self.stream.current.type == 'sub':
|
|
||||||
self.fail('Block names in Jinja have to be valid Python '
|
|
||||||
'identifiers and may not contain hypens, use an '
|
|
||||||
'underscore instead.')
|
|
||||||
|
|
||||||
node.body = self.parse_statements(('name:endblock',), drop_needle=True)
|
|
||||||
self.stream.skip_if('name:' + node.name)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_extends(self):
|
|
||||||
node = nodes.Extends(lineno=next(self.stream).lineno)
|
|
||||||
node.template = self.parse_expression()
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_import_context(self, node, default):
|
|
||||||
if self.stream.current.test_any('name:with', 'name:without') and \
|
|
||||||
self.stream.look().test('name:context'):
|
|
||||||
node.with_context = next(self.stream).value == 'with'
|
|
||||||
self.stream.skip()
|
|
||||||
else:
|
|
||||||
node.with_context = default
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_include(self):
|
|
||||||
node = nodes.Include(lineno=next(self.stream).lineno)
|
|
||||||
node.template = self.parse_expression()
|
|
||||||
if self.stream.current.test('name:ignore') and \
|
|
||||||
self.stream.look().test('name:missing'):
|
|
||||||
node.ignore_missing = True
|
|
||||||
self.stream.skip(2)
|
|
||||||
else:
|
|
||||||
node.ignore_missing = False
|
|
||||||
return self.parse_import_context(node, True)
|
|
||||||
|
|
||||||
def parse_import(self):
|
|
||||||
node = nodes.Import(lineno=next(self.stream).lineno)
|
|
||||||
node.template = self.parse_expression()
|
|
||||||
self.stream.expect('name:as')
|
|
||||||
node.target = self.parse_assign_target(name_only=True).name
|
|
||||||
return self.parse_import_context(node, False)
|
|
||||||
|
|
||||||
def parse_from(self):
|
|
||||||
node = nodes.FromImport(lineno=next(self.stream).lineno)
|
|
||||||
node.template = self.parse_expression()
|
|
||||||
self.stream.expect('name:import')
|
|
||||||
node.names = []
|
|
||||||
|
|
||||||
def parse_context():
|
|
||||||
if self.stream.current.value in ('with', 'without') and \
|
|
||||||
self.stream.look().test('name:context'):
|
|
||||||
node.with_context = next(self.stream).value == 'with'
|
|
||||||
self.stream.skip()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
while 1:
|
|
||||||
if node.names:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
if self.stream.current.type == 'name':
|
|
||||||
if parse_context():
|
|
||||||
break
|
|
||||||
target = self.parse_assign_target(name_only=True)
|
|
||||||
if target.name.startswith('_'):
|
|
||||||
self.fail('names starting with an underline can not '
|
|
||||||
'be imported', target.lineno,
|
|
||||||
exc=TemplateAssertionError)
|
|
||||||
if self.stream.skip_if('name:as'):
|
|
||||||
alias = self.parse_assign_target(name_only=True)
|
|
||||||
node.names.append((target.name, alias.name))
|
|
||||||
else:
|
|
||||||
node.names.append(target.name)
|
|
||||||
if parse_context() or self.stream.current.type != 'comma':
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
if not hasattr(node, 'with_context'):
|
|
||||||
node.with_context = False
|
|
||||||
self.stream.skip_if('comma')
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_signature(self, node):
|
|
||||||
node.args = args = []
|
|
||||||
node.defaults = defaults = []
|
|
||||||
self.stream.expect('lparen')
|
|
||||||
while self.stream.current.type != 'rparen':
|
|
||||||
if args:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
arg = self.parse_assign_target(name_only=True)
|
|
||||||
arg.set_ctx('param')
|
|
||||||
if self.stream.skip_if('assign'):
|
|
||||||
defaults.append(self.parse_expression())
|
|
||||||
args.append(arg)
|
|
||||||
self.stream.expect('rparen')
|
|
||||||
|
|
||||||
def parse_call_block(self):
|
|
||||||
node = nodes.CallBlock(lineno=next(self.stream).lineno)
|
|
||||||
if self.stream.current.type == 'lparen':
|
|
||||||
self.parse_signature(node)
|
|
||||||
else:
|
|
||||||
node.args = []
|
|
||||||
node.defaults = []
|
|
||||||
|
|
||||||
node.call = self.parse_expression()
|
|
||||||
if not isinstance(node.call, nodes.Call):
|
|
||||||
self.fail('expected call', node.lineno)
|
|
||||||
node.body = self.parse_statements(('name:endcall',), drop_needle=True)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_filter_block(self):
|
|
||||||
node = nodes.FilterBlock(lineno=next(self.stream).lineno)
|
|
||||||
node.filter = self.parse_filter(None, start_inline=True)
|
|
||||||
node.body = self.parse_statements(('name:endfilter',),
|
|
||||||
drop_needle=True)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_macro(self):
|
|
||||||
node = nodes.Macro(lineno=next(self.stream).lineno)
|
|
||||||
node.name = self.parse_assign_target(name_only=True).name
|
|
||||||
self.parse_signature(node)
|
|
||||||
node.body = self.parse_statements(('name:endmacro',),
|
|
||||||
drop_needle=True)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_print(self):
|
|
||||||
node = nodes.Output(lineno=next(self.stream).lineno)
|
|
||||||
node.nodes = []
|
|
||||||
while self.stream.current.type != 'block_end':
|
|
||||||
if node.nodes:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
node.nodes.append(self.parse_expression())
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_assign_target(self, with_tuple=True, name_only=False,
|
|
||||||
extra_end_rules=None):
|
|
||||||
"""Parse an assignment target. As Jinja2 allows assignments to
|
|
||||||
tuples, this function can parse all allowed assignment targets. Per
|
|
||||||
default assignments to tuples are parsed, that can be disable however
|
|
||||||
by setting `with_tuple` to `False`. If only assignments to names are
|
|
||||||
wanted `name_only` can be set to `True`. The `extra_end_rules`
|
|
||||||
parameter is forwarded to the tuple parsing function.
|
|
||||||
"""
|
|
||||||
if name_only:
|
|
||||||
token = self.stream.expect('name')
|
|
||||||
target = nodes.Name(token.value, 'store', lineno=token.lineno)
|
|
||||||
else:
|
|
||||||
if with_tuple:
|
|
||||||
target = self.parse_tuple(simplified=True,
|
|
||||||
extra_end_rules=extra_end_rules)
|
|
||||||
else:
|
|
||||||
target = self.parse_primary(with_postfix=False)
|
|
||||||
target.set_ctx('store')
|
|
||||||
if not target.can_assign():
|
|
||||||
self.fail('can\'t assign to %r' % target.__class__.
|
|
||||||
__name__.lower(), target.lineno)
|
|
||||||
return target
|
|
||||||
|
|
||||||
def parse_expression(self, with_condexpr=True):
|
|
||||||
"""Parse an expression. Per default all expressions are parsed, if
|
|
||||||
the optional `with_condexpr` parameter is set to `False` conditional
|
|
||||||
expressions are not parsed.
|
|
||||||
"""
|
|
||||||
if with_condexpr:
|
|
||||||
return self.parse_condexpr()
|
|
||||||
return self.parse_or()
|
|
||||||
|
|
||||||
def parse_condexpr(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
expr1 = self.parse_or()
|
|
||||||
while self.stream.skip_if('name:if'):
|
|
||||||
expr2 = self.parse_or()
|
|
||||||
if self.stream.skip_if('name:else'):
|
|
||||||
expr3 = self.parse_condexpr()
|
|
||||||
else:
|
|
||||||
expr3 = None
|
|
||||||
expr1 = nodes.CondExpr(expr2, expr1, expr3, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return expr1
|
|
||||||
|
|
||||||
def parse_or(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_and()
|
|
||||||
while self.stream.skip_if('name:or'):
|
|
||||||
right = self.parse_and()
|
|
||||||
left = nodes.Or(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_and(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_not()
|
|
||||||
while self.stream.skip_if('name:and'):
|
|
||||||
right = self.parse_not()
|
|
||||||
left = nodes.And(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_not(self):
|
|
||||||
if self.stream.current.test('name:not'):
|
|
||||||
lineno = next(self.stream).lineno
|
|
||||||
return nodes.Not(self.parse_not(), lineno=lineno)
|
|
||||||
return self.parse_compare()
|
|
||||||
|
|
||||||
def parse_compare(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
expr = self.parse_add()
|
|
||||||
ops = []
|
|
||||||
while 1:
|
|
||||||
token_type = self.stream.current.type
|
|
||||||
if token_type in _compare_operators:
|
|
||||||
next(self.stream)
|
|
||||||
ops.append(nodes.Operand(token_type, self.parse_add()))
|
|
||||||
elif self.stream.skip_if('name:in'):
|
|
||||||
ops.append(nodes.Operand('in', self.parse_add()))
|
|
||||||
elif self.stream.current.test('name:not') and \
|
|
||||||
self.stream.look().test('name:in'):
|
|
||||||
self.stream.skip(2)
|
|
||||||
ops.append(nodes.Operand('notin', self.parse_add()))
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
if not ops:
|
|
||||||
return expr
|
|
||||||
return nodes.Compare(expr, ops, lineno=lineno)
|
|
||||||
|
|
||||||
def parse_add(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_sub()
|
|
||||||
while self.stream.current.type == 'add':
|
|
||||||
next(self.stream)
|
|
||||||
right = self.parse_sub()
|
|
||||||
left = nodes.Add(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_sub(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_concat()
|
|
||||||
while self.stream.current.type == 'sub':
|
|
||||||
next(self.stream)
|
|
||||||
right = self.parse_concat()
|
|
||||||
left = nodes.Sub(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_concat(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
args = [self.parse_mul()]
|
|
||||||
while self.stream.current.type == 'tilde':
|
|
||||||
next(self.stream)
|
|
||||||
args.append(self.parse_mul())
|
|
||||||
if len(args) == 1:
|
|
||||||
return args[0]
|
|
||||||
return nodes.Concat(args, lineno=lineno)
|
|
||||||
|
|
||||||
def parse_mul(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_div()
|
|
||||||
while self.stream.current.type == 'mul':
|
|
||||||
next(self.stream)
|
|
||||||
right = self.parse_div()
|
|
||||||
left = nodes.Mul(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_div(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_floordiv()
|
|
||||||
while self.stream.current.type == 'div':
|
|
||||||
next(self.stream)
|
|
||||||
right = self.parse_floordiv()
|
|
||||||
left = nodes.Div(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_floordiv(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_mod()
|
|
||||||
while self.stream.current.type == 'floordiv':
|
|
||||||
next(self.stream)
|
|
||||||
right = self.parse_mod()
|
|
||||||
left = nodes.FloorDiv(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_mod(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_pow()
|
|
||||||
while self.stream.current.type == 'mod':
|
|
||||||
next(self.stream)
|
|
||||||
right = self.parse_pow()
|
|
||||||
left = nodes.Mod(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_pow(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
left = self.parse_unary()
|
|
||||||
while self.stream.current.type == 'pow':
|
|
||||||
next(self.stream)
|
|
||||||
right = self.parse_unary()
|
|
||||||
left = nodes.Pow(left, right, lineno=lineno)
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_unary(self):
|
|
||||||
token_type = self.stream.current.type
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
if token_type == 'sub':
|
|
||||||
next(self.stream)
|
|
||||||
node = self.parse_unary()
|
|
||||||
return nodes.Neg(node, lineno=lineno)
|
|
||||||
if token_type == 'add':
|
|
||||||
next(self.stream)
|
|
||||||
node = self.parse_unary()
|
|
||||||
return nodes.Pos(node, lineno=lineno)
|
|
||||||
return self.parse_primary()
|
|
||||||
|
|
||||||
def parse_primary(self, with_postfix=True):
|
|
||||||
token = self.stream.current
|
|
||||||
if token.type == 'name':
|
|
||||||
if token.value in ('true', 'false', 'True', 'False'):
|
|
||||||
node = nodes.Const(token.value in ('true', 'True'),
|
|
||||||
lineno=token.lineno)
|
|
||||||
elif token.value in ('none', 'None'):
|
|
||||||
node = nodes.Const(None, lineno=token.lineno)
|
|
||||||
else:
|
|
||||||
node = nodes.Name(token.value, 'load', lineno=token.lineno)
|
|
||||||
next(self.stream)
|
|
||||||
elif token.type == 'string':
|
|
||||||
next(self.stream)
|
|
||||||
buf = [token.value]
|
|
||||||
lineno = token.lineno
|
|
||||||
while self.stream.current.type == 'string':
|
|
||||||
buf.append(self.stream.current.value)
|
|
||||||
next(self.stream)
|
|
||||||
node = nodes.Const(''.join(buf), lineno=lineno)
|
|
||||||
elif token.type in ('integer', 'float'):
|
|
||||||
next(self.stream)
|
|
||||||
node = nodes.Const(token.value, lineno=token.lineno)
|
|
||||||
elif token.type == 'lparen':
|
|
||||||
next(self.stream)
|
|
||||||
node = self.parse_tuple(explicit_parentheses=True)
|
|
||||||
self.stream.expect('rparen')
|
|
||||||
elif token.type == 'lbracket':
|
|
||||||
node = self.parse_list()
|
|
||||||
elif token.type == 'lbrace':
|
|
||||||
node = self.parse_dict()
|
|
||||||
else:
|
|
||||||
self.fail("unexpected '%s'" % describe_token(token), token.lineno)
|
|
||||||
if with_postfix:
|
|
||||||
node = self.parse_postfix(node)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_tuple(self, simplified=False, with_condexpr=True,
|
|
||||||
extra_end_rules=None, explicit_parentheses=False):
|
|
||||||
"""Works like `parse_expression` but if multiple expressions are
|
|
||||||
delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
|
|
||||||
This method could also return a regular expression instead of a tuple
|
|
||||||
if no commas where found.
|
|
||||||
|
|
||||||
The default parsing mode is a full tuple. If `simplified` is `True`
|
|
||||||
only names and literals are parsed. The `no_condexpr` parameter is
|
|
||||||
forwarded to :meth:`parse_expression`.
|
|
||||||
|
|
||||||
Because tuples do not require delimiters and may end in a bogus comma
|
|
||||||
an extra hint is needed that marks the end of a tuple. For example
|
|
||||||
for loops support tuples between `for` and `in`. In that case the
|
|
||||||
`extra_end_rules` is set to ``['name:in']``.
|
|
||||||
|
|
||||||
`explicit_parentheses` is true if the parsing was triggered by an
|
|
||||||
expression in parentheses. This is used to figure out if an empty
|
|
||||||
tuple is a valid expression or not.
|
|
||||||
"""
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
if simplified:
|
|
||||||
parse = lambda: self.parse_primary(with_postfix=False)
|
|
||||||
elif with_condexpr:
|
|
||||||
parse = self.parse_expression
|
|
||||||
else:
|
|
||||||
parse = lambda: self.parse_expression(with_condexpr=False)
|
|
||||||
args = []
|
|
||||||
is_tuple = False
|
|
||||||
while 1:
|
|
||||||
if args:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
if self.is_tuple_end(extra_end_rules):
|
|
||||||
break
|
|
||||||
args.append(parse())
|
|
||||||
if self.stream.current.type == 'comma':
|
|
||||||
is_tuple = True
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
|
|
||||||
if not is_tuple:
|
|
||||||
if args:
|
|
||||||
return args[0]
|
|
||||||
|
|
||||||
# if we don't have explicit parentheses, an empty tuple is
|
|
||||||
# not a valid expression. This would mean nothing (literally
|
|
||||||
# nothing) in the spot of an expression would be an empty
|
|
||||||
# tuple.
|
|
||||||
if not explicit_parentheses:
|
|
||||||
self.fail('Expected an expression, got \'%s\'' %
|
|
||||||
describe_token(self.stream.current))
|
|
||||||
|
|
||||||
return nodes.Tuple(args, 'load', lineno=lineno)
|
|
||||||
|
|
||||||
def parse_list(self):
|
|
||||||
token = self.stream.expect('lbracket')
|
|
||||||
items = []
|
|
||||||
while self.stream.current.type != 'rbracket':
|
|
||||||
if items:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
if self.stream.current.type == 'rbracket':
|
|
||||||
break
|
|
||||||
items.append(self.parse_expression())
|
|
||||||
self.stream.expect('rbracket')
|
|
||||||
return nodes.List(items, lineno=token.lineno)
|
|
||||||
|
|
||||||
def parse_dict(self):
|
|
||||||
token = self.stream.expect('lbrace')
|
|
||||||
items = []
|
|
||||||
while self.stream.current.type != 'rbrace':
|
|
||||||
if items:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
if self.stream.current.type == 'rbrace':
|
|
||||||
break
|
|
||||||
key = self.parse_expression()
|
|
||||||
self.stream.expect('colon')
|
|
||||||
value = self.parse_expression()
|
|
||||||
items.append(nodes.Pair(key, value, lineno=key.lineno))
|
|
||||||
self.stream.expect('rbrace')
|
|
||||||
return nodes.Dict(items, lineno=token.lineno)
|
|
||||||
|
|
||||||
def parse_postfix(self, node):
|
|
||||||
while 1:
|
|
||||||
token_type = self.stream.current.type
|
|
||||||
if token_type == 'dot' or token_type == 'lbracket':
|
|
||||||
node = self.parse_subscript(node)
|
|
||||||
elif token_type == 'lparen':
|
|
||||||
node = self.parse_call(node)
|
|
||||||
elif token_type == 'pipe':
|
|
||||||
node = self.parse_filter(node)
|
|
||||||
elif token_type == 'name' and self.stream.current.value == 'is':
|
|
||||||
node = self.parse_test(node)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_subscript(self, node):
|
|
||||||
token = next(self.stream)
|
|
||||||
if token.type == 'dot':
|
|
||||||
attr_token = self.stream.current
|
|
||||||
next(self.stream)
|
|
||||||
if attr_token.type == 'name':
|
|
||||||
return nodes.Getattr(node, attr_token.value, 'load',
|
|
||||||
lineno=token.lineno)
|
|
||||||
elif attr_token.type != 'integer':
|
|
||||||
self.fail('expected name or number', attr_token.lineno)
|
|
||||||
arg = nodes.Const(attr_token.value, lineno=attr_token.lineno)
|
|
||||||
return nodes.Getitem(node, arg, 'load', lineno=token.lineno)
|
|
||||||
if token.type == 'lbracket':
|
|
||||||
priority_on_attribute = False
|
|
||||||
args = []
|
|
||||||
while self.stream.current.type != 'rbracket':
|
|
||||||
if args:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
args.append(self.parse_subscribed())
|
|
||||||
self.stream.expect('rbracket')
|
|
||||||
if len(args) == 1:
|
|
||||||
arg = args[0]
|
|
||||||
else:
|
|
||||||
arg = nodes.Tuple(args, 'load', lineno=token.lineno)
|
|
||||||
return nodes.Getitem(node, arg, 'load', lineno=token.lineno)
|
|
||||||
self.fail('expected subscript expression', self.lineno)
|
|
||||||
|
|
||||||
def parse_subscribed(self):
|
|
||||||
lineno = self.stream.current.lineno
|
|
||||||
|
|
||||||
if self.stream.current.type == 'colon':
|
|
||||||
next(self.stream)
|
|
||||||
args = [None]
|
|
||||||
else:
|
|
||||||
node = self.parse_expression()
|
|
||||||
if self.stream.current.type != 'colon':
|
|
||||||
return node
|
|
||||||
next(self.stream)
|
|
||||||
args = [node]
|
|
||||||
|
|
||||||
if self.stream.current.type == 'colon':
|
|
||||||
args.append(None)
|
|
||||||
elif self.stream.current.type not in ('rbracket', 'comma'):
|
|
||||||
args.append(self.parse_expression())
|
|
||||||
else:
|
|
||||||
args.append(None)
|
|
||||||
|
|
||||||
if self.stream.current.type == 'colon':
|
|
||||||
next(self.stream)
|
|
||||||
if self.stream.current.type not in ('rbracket', 'comma'):
|
|
||||||
args.append(self.parse_expression())
|
|
||||||
else:
|
|
||||||
args.append(None)
|
|
||||||
else:
|
|
||||||
args.append(None)
|
|
||||||
|
|
||||||
return nodes.Slice(lineno=lineno, *args)
|
|
||||||
|
|
||||||
def parse_call(self, node):
|
|
||||||
token = self.stream.expect('lparen')
|
|
||||||
args = []
|
|
||||||
kwargs = []
|
|
||||||
dyn_args = dyn_kwargs = None
|
|
||||||
require_comma = False
|
|
||||||
|
|
||||||
def ensure(expr):
|
|
||||||
if not expr:
|
|
||||||
self.fail('invalid syntax for function call expression',
|
|
||||||
token.lineno)
|
|
||||||
|
|
||||||
while self.stream.current.type != 'rparen':
|
|
||||||
if require_comma:
|
|
||||||
self.stream.expect('comma')
|
|
||||||
# support for trailing comma
|
|
||||||
if self.stream.current.type == 'rparen':
|
|
||||||
break
|
|
||||||
if self.stream.current.type == 'mul':
|
|
||||||
ensure(dyn_args is None and dyn_kwargs is None)
|
|
||||||
next(self.stream)
|
|
||||||
dyn_args = self.parse_expression()
|
|
||||||
elif self.stream.current.type == 'pow':
|
|
||||||
ensure(dyn_kwargs is None)
|
|
||||||
next(self.stream)
|
|
||||||
dyn_kwargs = self.parse_expression()
|
|
||||||
else:
|
|
||||||
ensure(dyn_args is None and dyn_kwargs is None)
|
|
||||||
if self.stream.current.type == 'name' and \
|
|
||||||
self.stream.look().type == 'assign':
|
|
||||||
key = self.stream.current.value
|
|
||||||
self.stream.skip(2)
|
|
||||||
value = self.parse_expression()
|
|
||||||
kwargs.append(nodes.Keyword(key, value,
|
|
||||||
lineno=value.lineno))
|
|
||||||
else:
|
|
||||||
ensure(not kwargs)
|
|
||||||
args.append(self.parse_expression())
|
|
||||||
|
|
||||||
require_comma = True
|
|
||||||
self.stream.expect('rparen')
|
|
||||||
|
|
||||||
if node is None:
|
|
||||||
return args, kwargs, dyn_args, dyn_kwargs
|
|
||||||
return nodes.Call(node, args, kwargs, dyn_args, dyn_kwargs,
|
|
||||||
lineno=token.lineno)
|
|
||||||
|
|
||||||
def parse_filter(self, node, start_inline=False):
|
|
||||||
while self.stream.current.type == 'pipe' or start_inline:
|
|
||||||
if not start_inline:
|
|
||||||
next(self.stream)
|
|
||||||
token = self.stream.expect('name')
|
|
||||||
name = token.value
|
|
||||||
while self.stream.current.type == 'dot':
|
|
||||||
next(self.stream)
|
|
||||||
name += '.' + self.stream.expect('name').value
|
|
||||||
if self.stream.current.type == 'lparen':
|
|
||||||
args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None)
|
|
||||||
else:
|
|
||||||
args = []
|
|
||||||
kwargs = []
|
|
||||||
dyn_args = dyn_kwargs = None
|
|
||||||
node = nodes.Filter(node, name, args, kwargs, dyn_args,
|
|
||||||
dyn_kwargs, lineno=token.lineno)
|
|
||||||
start_inline = False
|
|
||||||
return node
|
|
||||||
|
|
||||||
def parse_test(self, node):
|
|
||||||
token = next(self.stream)
|
|
||||||
if self.stream.current.test('name:not'):
|
|
||||||
next(self.stream)
|
|
||||||
negated = True
|
|
||||||
else:
|
|
||||||
negated = False
|
|
||||||
name = self.stream.expect('name').value
|
|
||||||
while self.stream.current.type == 'dot':
|
|
||||||
next(self.stream)
|
|
||||||
name += '.' + self.stream.expect('name').value
|
|
||||||
dyn_args = dyn_kwargs = None
|
|
||||||
kwargs = []
|
|
||||||
if self.stream.current.type == 'lparen':
|
|
||||||
args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None)
|
|
||||||
elif self.stream.current.type in ('name', 'string', 'integer',
|
|
||||||
'float', 'lparen', 'lbracket',
|
|
||||||
'lbrace') and not \
|
|
||||||
self.stream.current.test_any('name:else', 'name:or',
|
|
||||||
'name:and'):
|
|
||||||
if self.stream.current.test('name:is'):
|
|
||||||
self.fail('You cannot chain multiple tests with is')
|
|
||||||
args = [self.parse_expression()]
|
|
||||||
else:
|
|
||||||
args = []
|
|
||||||
node = nodes.Test(node, name, args, kwargs, dyn_args,
|
|
||||||
dyn_kwargs, lineno=token.lineno)
|
|
||||||
if negated:
|
|
||||||
node = nodes.Not(node, lineno=token.lineno)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def subparse(self, end_tokens=None):
|
|
||||||
body = []
|
|
||||||
data_buffer = []
|
|
||||||
add_data = data_buffer.append
|
|
||||||
|
|
||||||
if end_tokens is not None:
|
|
||||||
self._end_token_stack.append(end_tokens)
|
|
||||||
|
|
||||||
def flush_data():
|
|
||||||
if data_buffer:
|
|
||||||
lineno = data_buffer[0].lineno
|
|
||||||
body.append(nodes.Output(data_buffer[:], lineno=lineno))
|
|
||||||
del data_buffer[:]
|
|
||||||
|
|
||||||
try:
|
|
||||||
while self.stream:
|
|
||||||
token = self.stream.current
|
|
||||||
if token.type == 'data':
|
|
||||||
if token.value:
|
|
||||||
add_data(nodes.TemplateData(token.value,
|
|
||||||
lineno=token.lineno))
|
|
||||||
next(self.stream)
|
|
||||||
elif token.type == 'variable_begin':
|
|
||||||
next(self.stream)
|
|
||||||
add_data(self.parse_tuple(with_condexpr=True))
|
|
||||||
self.stream.expect('variable_end')
|
|
||||||
elif token.type == 'block_begin':
|
|
||||||
flush_data()
|
|
||||||
next(self.stream)
|
|
||||||
if end_tokens is not None and \
|
|
||||||
self.stream.current.test_any(*end_tokens):
|
|
||||||
return body
|
|
||||||
rv = self.parse_statement()
|
|
||||||
if isinstance(rv, list):
|
|
||||||
body.extend(rv)
|
|
||||||
else:
|
|
||||||
body.append(rv)
|
|
||||||
self.stream.expect('block_end')
|
|
||||||
else:
|
|
||||||
raise AssertionError('internal parsing error')
|
|
||||||
|
|
||||||
flush_data()
|
|
||||||
finally:
|
|
||||||
if end_tokens is not None:
|
|
||||||
self._end_token_stack.pop()
|
|
||||||
|
|
||||||
return body
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
"""Parse the whole template into a `Template` node."""
|
|
||||||
result = nodes.Template(self.subparse(), lineno=1)
|
|
||||||
result.set_environment(self.environment)
|
|
||||||
return result
|
|
|
@ -1,537 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.runtime
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Runtime helpers.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
from itertools import chain, imap
|
|
||||||
from jinja2.nodes import EvalContext
|
|
||||||
from jinja2.utils import Markup, partial, soft_unicode, escape, missing, \
|
|
||||||
concat, MethodType, FunctionType, internalcode, next, \
|
|
||||||
object_type_repr
|
|
||||||
from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
|
|
||||||
TemplateNotFound
|
|
||||||
|
|
||||||
|
|
||||||
# these variables are exported to the template runtime
|
|
||||||
__all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup',
|
|
||||||
'TemplateRuntimeError', 'missing', 'concat', 'escape',
|
|
||||||
'markup_join', 'unicode_join', 'to_string',
|
|
||||||
'TemplateNotFound']
|
|
||||||
|
|
||||||
|
|
||||||
#: the types we support for context functions
|
|
||||||
_context_function_types = (FunctionType, MethodType)
|
|
||||||
|
|
||||||
#: the name of the function that is used to convert something into
|
|
||||||
#: a string. 2to3 will adopt that automatically and the generated
|
|
||||||
#: code can take advantage of it.
|
|
||||||
to_string = unicode
|
|
||||||
|
|
||||||
|
|
||||||
def markup_join(seq):
|
|
||||||
"""Concatenation that escapes if necessary and converts to unicode."""
|
|
||||||
buf = []
|
|
||||||
iterator = imap(soft_unicode, seq)
|
|
||||||
for arg in iterator:
|
|
||||||
buf.append(arg)
|
|
||||||
if hasattr(arg, '__html__'):
|
|
||||||
return Markup(u'').join(chain(buf, iterator))
|
|
||||||
return concat(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def unicode_join(seq):
|
|
||||||
"""Simple args to unicode conversion and concatenation."""
|
|
||||||
return concat(imap(unicode, seq))
|
|
||||||
|
|
||||||
|
|
||||||
def new_context(environment, template_name, blocks, vars=None,
|
|
||||||
shared=None, globals=None, locals=None):
|
|
||||||
"""Internal helper to for context creation."""
|
|
||||||
if vars is None:
|
|
||||||
vars = {}
|
|
||||||
if shared:
|
|
||||||
parent = vars
|
|
||||||
else:
|
|
||||||
parent = dict(globals or (), **vars)
|
|
||||||
if locals:
|
|
||||||
# if the parent is shared a copy should be created because
|
|
||||||
# we don't want to modify the dict passed
|
|
||||||
if shared:
|
|
||||||
parent = dict(parent)
|
|
||||||
for key, value in locals.iteritems():
|
|
||||||
if key[:2] == 'l_' and value is not missing:
|
|
||||||
parent[key[2:]] = value
|
|
||||||
return Context(environment, parent, template_name, blocks)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateReference(object):
|
|
||||||
"""The `self` in templates."""
|
|
||||||
|
|
||||||
def __init__(self, context):
|
|
||||||
self.__context = context
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
blocks = self.__context.blocks[name]
|
|
||||||
wrap = self.__context.eval_ctx.autoescape and \
|
|
||||||
Markup or (lambda x: x)
|
|
||||||
return BlockReference(name, self.__context, blocks, 0)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s %r>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.__context.name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Context(object):
|
|
||||||
"""The template context holds the variables of a template. It stores the
|
|
||||||
values passed to the template and also the names the template exports.
|
|
||||||
Creating instances is neither supported nor useful as it's created
|
|
||||||
automatically at various stages of the template evaluation and should not
|
|
||||||
be created by hand.
|
|
||||||
|
|
||||||
The context is immutable. Modifications on :attr:`parent` **must not**
|
|
||||||
happen and modifications on :attr:`vars` are allowed from generated
|
|
||||||
template code only. Template filters and global functions marked as
|
|
||||||
:func:`contextfunction`\s get the active context passed as first argument
|
|
||||||
and are allowed to access the context read-only.
|
|
||||||
|
|
||||||
The template context supports read only dict operations (`get`,
|
|
||||||
`keys`, `values`, `items`, `iterkeys`, `itervalues`, `iteritems`,
|
|
||||||
`__getitem__`, `__contains__`). Additionally there is a :meth:`resolve`
|
|
||||||
method that doesn't fail with a `KeyError` but returns an
|
|
||||||
:class:`Undefined` object for missing variables.
|
|
||||||
"""
|
|
||||||
__slots__ = ('parent', 'vars', 'environment', 'eval_ctx', 'exported_vars',
|
|
||||||
'name', 'blocks', '__weakref__')
|
|
||||||
|
|
||||||
def __init__(self, environment, parent, name, blocks):
|
|
||||||
self.parent = parent
|
|
||||||
self.vars = {}
|
|
||||||
self.environment = environment
|
|
||||||
self.eval_ctx = EvalContext(self.environment, name)
|
|
||||||
self.exported_vars = set()
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
# create the initial mapping of blocks. Whenever template inheritance
|
|
||||||
# takes place the runtime will update this mapping with the new blocks
|
|
||||||
# from the template.
|
|
||||||
self.blocks = dict((k, [v]) for k, v in blocks.iteritems())
|
|
||||||
|
|
||||||
def super(self, name, current):
|
|
||||||
"""Render a parent block."""
|
|
||||||
try:
|
|
||||||
blocks = self.blocks[name]
|
|
||||||
index = blocks.index(current) + 1
|
|
||||||
blocks[index]
|
|
||||||
except LookupError:
|
|
||||||
return self.environment.undefined('there is no parent block '
|
|
||||||
'called %r.' % name,
|
|
||||||
name='super')
|
|
||||||
return BlockReference(name, self, blocks, index)
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
"""Returns an item from the template context, if it doesn't exist
|
|
||||||
`default` is returned.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self[key]
|
|
||||||
except KeyError:
|
|
||||||
return default
|
|
||||||
|
|
||||||
def resolve(self, key):
|
|
||||||
"""Looks up a variable like `__getitem__` or `get` but returns an
|
|
||||||
:class:`Undefined` object with the name of the name looked up.
|
|
||||||
"""
|
|
||||||
if key in self.vars:
|
|
||||||
return self.vars[key]
|
|
||||||
if key in self.parent:
|
|
||||||
return self.parent[key]
|
|
||||||
return self.environment.undefined(name=key)
|
|
||||||
|
|
||||||
def get_exported(self):
|
|
||||||
"""Get a new dict with the exported variables."""
|
|
||||||
return dict((k, self.vars[k]) for k in self.exported_vars)
|
|
||||||
|
|
||||||
def get_all(self):
|
|
||||||
"""Return a copy of the complete context as dict including the
|
|
||||||
exported variables.
|
|
||||||
"""
|
|
||||||
return dict(self.parent, **self.vars)
|
|
||||||
|
|
||||||
@internalcode
|
|
||||||
def call(__self, __obj, *args, **kwargs):
|
|
||||||
"""Call the callable with the arguments and keyword arguments
|
|
||||||
provided but inject the active context or environment as first
|
|
||||||
argument if the callable is a :func:`contextfunction` or
|
|
||||||
:func:`environmentfunction`.
|
|
||||||
"""
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
if isinstance(__obj, _context_function_types):
|
|
||||||
if getattr(__obj, 'contextfunction', 0):
|
|
||||||
args = (__self,) + args
|
|
||||||
elif getattr(__obj, 'evalcontextfunction', 0):
|
|
||||||
args = (__self.eval_ctx,) + args
|
|
||||||
elif getattr(__obj, 'environmentfunction', 0):
|
|
||||||
args = (__self.environment,) + args
|
|
||||||
return __obj(*args, **kwargs)
|
|
||||||
|
|
||||||
def derived(self, locals=None):
|
|
||||||
"""Internal helper function to create a derived context."""
|
|
||||||
context = new_context(self.environment, self.name, {},
|
|
||||||
self.parent, True, None, locals)
|
|
||||||
context.eval_ctx = self.eval_ctx
|
|
||||||
context.blocks.update((k, list(v)) for k, v in self.blocks.iteritems())
|
|
||||||
return context
|
|
||||||
|
|
||||||
def _all(meth):
|
|
||||||
proxy = lambda self: getattr(self.get_all(), meth)()
|
|
||||||
proxy.__doc__ = getattr(dict, meth).__doc__
|
|
||||||
proxy.__name__ = meth
|
|
||||||
return proxy
|
|
||||||
|
|
||||||
keys = _all('keys')
|
|
||||||
values = _all('values')
|
|
||||||
items = _all('items')
|
|
||||||
|
|
||||||
# not available on python 3
|
|
||||||
if hasattr(dict, 'iterkeys'):
|
|
||||||
iterkeys = _all('iterkeys')
|
|
||||||
itervalues = _all('itervalues')
|
|
||||||
iteritems = _all('iteritems')
|
|
||||||
del _all
|
|
||||||
|
|
||||||
def __contains__(self, name):
|
|
||||||
return name in self.vars or name in self.parent
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
"""Lookup a variable or raise `KeyError` if the variable is
|
|
||||||
undefined.
|
|
||||||
"""
|
|
||||||
item = self.resolve(key)
|
|
||||||
if isinstance(item, Undefined):
|
|
||||||
raise KeyError(key)
|
|
||||||
return item
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s %s of %r>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
repr(self.get_all()),
|
|
||||||
self.name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# register the context as mapping if possible
|
|
||||||
try:
|
|
||||||
from collections import Mapping
|
|
||||||
Mapping.register(Context)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BlockReference(object):
|
|
||||||
"""One block on a template reference."""
|
|
||||||
|
|
||||||
def __init__(self, name, context, stack, depth):
|
|
||||||
self.name = name
|
|
||||||
self._context = context
|
|
||||||
self._stack = stack
|
|
||||||
self._depth = depth
|
|
||||||
|
|
||||||
@property
|
|
||||||
def super(self):
|
|
||||||
"""Super the block."""
|
|
||||||
if self._depth + 1 >= len(self._stack):
|
|
||||||
return self._context.environment. \
|
|
||||||
undefined('there is no parent block called %r.' %
|
|
||||||
self.name, name='super')
|
|
||||||
return BlockReference(self.name, self._context, self._stack,
|
|
||||||
self._depth + 1)
|
|
||||||
|
|
||||||
@internalcode
|
|
||||||
def __call__(self):
|
|
||||||
rv = concat(self._stack[self._depth](self._context))
|
|
||||||
if self._context.eval_ctx.autoescape:
|
|
||||||
rv = Markup(rv)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class LoopContext(object):
|
|
||||||
"""A loop context for dynamic iteration."""
|
|
||||||
|
|
||||||
def __init__(self, iterable, recurse=None):
|
|
||||||
self._iterator = iter(iterable)
|
|
||||||
self._recurse = recurse
|
|
||||||
self.index0 = -1
|
|
||||||
|
|
||||||
# try to get the length of the iterable early. This must be done
|
|
||||||
# here because there are some broken iterators around where there
|
|
||||||
# __len__ is the number of iterations left (i'm looking at your
|
|
||||||
# listreverseiterator!).
|
|
||||||
try:
|
|
||||||
self._length = len(iterable)
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
self._length = None
|
|
||||||
|
|
||||||
def cycle(self, *args):
|
|
||||||
"""Cycles among the arguments with the current loop index."""
|
|
||||||
if not args:
|
|
||||||
raise TypeError('no items for cycling given')
|
|
||||||
return args[self.index0 % len(args)]
|
|
||||||
|
|
||||||
first = property(lambda x: x.index0 == 0)
|
|
||||||
last = property(lambda x: x.index0 + 1 == x.length)
|
|
||||||
index = property(lambda x: x.index0 + 1)
|
|
||||||
revindex = property(lambda x: x.length - x.index0)
|
|
||||||
revindex0 = property(lambda x: x.length - x.index)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return self.length
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return LoopContextIterator(self)
|
|
||||||
|
|
||||||
@internalcode
|
|
||||||
def loop(self, iterable):
|
|
||||||
if self._recurse is None:
|
|
||||||
raise TypeError('Tried to call non recursive loop. Maybe you '
|
|
||||||
"forgot the 'recursive' modifier.")
|
|
||||||
return self._recurse(iterable, self._recurse)
|
|
||||||
|
|
||||||
# a nifty trick to enhance the error message if someone tried to call
|
|
||||||
# the the loop without or with too many arguments.
|
|
||||||
__call__ = loop
|
|
||||||
del loop
|
|
||||||
|
|
||||||
@property
|
|
||||||
def length(self):
|
|
||||||
if self._length is None:
|
|
||||||
# if was not possible to get the length of the iterator when
|
|
||||||
# the loop context was created (ie: iterating over a generator)
|
|
||||||
# we have to convert the iterable into a sequence and use the
|
|
||||||
# length of that.
|
|
||||||
iterable = tuple(self._iterator)
|
|
||||||
self._iterator = iter(iterable)
|
|
||||||
self._length = len(iterable) + self.index0 + 1
|
|
||||||
return self._length
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s %r/%r>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.index,
|
|
||||||
self.length
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LoopContextIterator(object):
|
|
||||||
"""The iterator for a loop context."""
|
|
||||||
__slots__ = ('context',)
|
|
||||||
|
|
||||||
def __init__(self, context):
|
|
||||||
self.context = context
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
ctx = self.context
|
|
||||||
ctx.index0 += 1
|
|
||||||
return next(ctx._iterator), ctx
|
|
||||||
|
|
||||||
|
|
||||||
class Macro(object):
|
|
||||||
"""Wraps a macro."""
|
|
||||||
|
|
||||||
def __init__(self, environment, func, name, arguments, defaults,
|
|
||||||
catch_kwargs, catch_varargs, caller):
|
|
||||||
self._environment = environment
|
|
||||||
self._func = func
|
|
||||||
self._argument_count = len(arguments)
|
|
||||||
self.name = name
|
|
||||||
self.arguments = arguments
|
|
||||||
self.defaults = defaults
|
|
||||||
self.catch_kwargs = catch_kwargs
|
|
||||||
self.catch_varargs = catch_varargs
|
|
||||||
self.caller = caller
|
|
||||||
|
|
||||||
@internalcode
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
arguments = []
|
|
||||||
for idx, name in enumerate(self.arguments):
|
|
||||||
try:
|
|
||||||
value = args[idx]
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
value = kwargs.pop(name)
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
value = self.defaults[idx - self._argument_count]
|
|
||||||
except:
|
|
||||||
value = self._environment.undefined(
|
|
||||||
'parameter %r was not provided' % name, name=name)
|
|
||||||
arguments.append(value)
|
|
||||||
|
|
||||||
# it's important that the order of these arguments does not change
|
|
||||||
# if not also changed in the compiler's `function_scoping` method.
|
|
||||||
# the order is caller, keyword arguments, positional arguments!
|
|
||||||
if self.caller:
|
|
||||||
caller = kwargs.pop('caller', None)
|
|
||||||
if caller is None:
|
|
||||||
caller = self._environment.undefined('No caller defined',
|
|
||||||
name='caller')
|
|
||||||
arguments.append(caller)
|
|
||||||
if self.catch_kwargs:
|
|
||||||
arguments.append(kwargs)
|
|
||||||
elif kwargs:
|
|
||||||
raise TypeError('macro %r takes no keyword argument %r' %
|
|
||||||
(self.name, next(iter(kwargs))))
|
|
||||||
if self.catch_varargs:
|
|
||||||
arguments.append(args[self._argument_count:])
|
|
||||||
elif len(args) > self._argument_count:
|
|
||||||
raise TypeError('macro %r takes not more than %d argument(s)' %
|
|
||||||
(self.name, len(self.arguments)))
|
|
||||||
return self._func(*arguments)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s %s>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.name is None and 'anonymous' or repr(self.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Undefined(object):
|
|
||||||
"""The default undefined type. This undefined type can be printed and
|
|
||||||
iterated over, but every other access will raise an :exc:`UndefinedError`:
|
|
||||||
|
|
||||||
>>> foo = Undefined(name='foo')
|
|
||||||
>>> str(foo)
|
|
||||||
''
|
|
||||||
>>> not foo
|
|
||||||
True
|
|
||||||
>>> foo + 42
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
UndefinedError: 'foo' is undefined
|
|
||||||
"""
|
|
||||||
__slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name',
|
|
||||||
'_undefined_exception')
|
|
||||||
|
|
||||||
def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError):
|
|
||||||
self._undefined_hint = hint
|
|
||||||
self._undefined_obj = obj
|
|
||||||
self._undefined_name = name
|
|
||||||
self._undefined_exception = exc
|
|
||||||
|
|
||||||
@internalcode
|
|
||||||
def _fail_with_undefined_error(self, *args, **kwargs):
|
|
||||||
"""Regular callback function for undefined objects that raises an
|
|
||||||
`UndefinedError` on call.
|
|
||||||
"""
|
|
||||||
if self._undefined_hint is None:
|
|
||||||
if self._undefined_obj is missing:
|
|
||||||
hint = '%r is undefined' % self._undefined_name
|
|
||||||
elif not isinstance(self._undefined_name, basestring):
|
|
||||||
hint = '%s has no element %r' % (
|
|
||||||
object_type_repr(self._undefined_obj),
|
|
||||||
self._undefined_name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
hint = '%r has no attribute %r' % (
|
|
||||||
object_type_repr(self._undefined_obj),
|
|
||||||
self._undefined_name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
hint = self._undefined_hint
|
|
||||||
raise self._undefined_exception(hint)
|
|
||||||
|
|
||||||
__add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
|
|
||||||
__truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
|
|
||||||
__mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
|
|
||||||
__getattr__ = __getitem__ = __lt__ = __le__ = __gt__ = __ge__ = \
|
|
||||||
__int__ = __float__ = __complex__ = __pow__ = __rpow__ = \
|
|
||||||
_fail_with_undefined_error
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return unicode(self).encode('utf-8')
|
|
||||||
|
|
||||||
# unicode goes after __str__ because we configured 2to3 to rename
|
|
||||||
# __unicode__ to __str__. because the 2to3 tree is not designed to
|
|
||||||
# remove nodes from it, we leave the above __str__ around and let
|
|
||||||
# it override at runtime.
|
|
||||||
def __unicode__(self):
|
|
||||||
return u''
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
if 0:
|
|
||||||
yield None
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Undefined'
|
|
||||||
|
|
||||||
|
|
||||||
class DebugUndefined(Undefined):
|
|
||||||
"""An undefined that returns the debug info when printed.
|
|
||||||
|
|
||||||
>>> foo = DebugUndefined(name='foo')
|
|
||||||
>>> str(foo)
|
|
||||||
'{{ foo }}'
|
|
||||||
>>> not foo
|
|
||||||
True
|
|
||||||
>>> foo + 42
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
UndefinedError: 'foo' is undefined
|
|
||||||
"""
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
if self._undefined_hint is None:
|
|
||||||
if self._undefined_obj is missing:
|
|
||||||
return u'{{ %s }}' % self._undefined_name
|
|
||||||
return '{{ no such element: %s[%r] }}' % (
|
|
||||||
object_type_repr(self._undefined_obj),
|
|
||||||
self._undefined_name
|
|
||||||
)
|
|
||||||
return u'{{ undefined value printed: %s }}' % self._undefined_hint
|
|
||||||
|
|
||||||
|
|
||||||
class StrictUndefined(Undefined):
|
|
||||||
"""An undefined that barks on print and iteration as well as boolean
|
|
||||||
tests and all kinds of comparisons. In other words: you can do nothing
|
|
||||||
with it except checking if it's defined using the `defined` test.
|
|
||||||
|
|
||||||
>>> foo = StrictUndefined(name='foo')
|
|
||||||
>>> str(foo)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
UndefinedError: 'foo' is undefined
|
|
||||||
>>> not foo
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
UndefinedError: 'foo' is undefined
|
|
||||||
>>> foo + 42
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
UndefinedError: 'foo' is undefined
|
|
||||||
"""
|
|
||||||
__slots__ = ()
|
|
||||||
__iter__ = __unicode__ = __str__ = __len__ = __nonzero__ = __eq__ = \
|
|
||||||
__ne__ = Undefined._fail_with_undefined_error
|
|
||||||
|
|
||||||
|
|
||||||
# remove remaining slots attributes, after the metaclass did the magic they
|
|
||||||
# are unneeded and irritating as they contain wrong data for the subclasses.
|
|
||||||
del Undefined.__slots__, DebugUndefined.__slots__, StrictUndefined.__slots__
|
|
|
@ -1,271 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.sandbox
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Adds a sandbox layer to Jinja as it was the default behavior in the old
|
|
||||||
Jinja 1 releases. This sandbox is slightly different from Jinja 1 as the
|
|
||||||
default behavior is easier to use.
|
|
||||||
|
|
||||||
The behavior can be changed by subclassing the environment.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD.
|
|
||||||
"""
|
|
||||||
import operator
|
|
||||||
from jinja2.runtime import Undefined
|
|
||||||
from jinja2.environment import Environment
|
|
||||||
from jinja2.exceptions import SecurityError
|
|
||||||
from jinja2.utils import FunctionType, MethodType, TracebackType, CodeType, \
|
|
||||||
FrameType, GeneratorType
|
|
||||||
|
|
||||||
|
|
||||||
#: maximum number of items a range may produce
|
|
||||||
MAX_RANGE = 100000
|
|
||||||
|
|
||||||
#: attributes of function objects that are considered unsafe.
|
|
||||||
UNSAFE_FUNCTION_ATTRIBUTES = set(['func_closure', 'func_code', 'func_dict',
|
|
||||||
'func_defaults', 'func_globals'])
|
|
||||||
|
|
||||||
#: unsafe method attributes. function attributes are unsafe for methods too
|
|
||||||
UNSAFE_METHOD_ATTRIBUTES = set(['im_class', 'im_func', 'im_self'])
|
|
||||||
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
# make sure we don't warn in python 2.6 about stuff we don't care about
|
|
||||||
warnings.filterwarnings('ignore', 'the sets module', DeprecationWarning,
|
|
||||||
module='jinja2.sandbox')
|
|
||||||
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
_mutable_set_types = (set,)
|
|
||||||
_mutable_mapping_types = (dict,)
|
|
||||||
_mutable_sequence_types = (list,)
|
|
||||||
|
|
||||||
|
|
||||||
# on python 2.x we can register the user collection types
|
|
||||||
try:
|
|
||||||
from UserDict import UserDict, DictMixin
|
|
||||||
from UserList import UserList
|
|
||||||
_mutable_mapping_types += (UserDict, DictMixin)
|
|
||||||
_mutable_set_types += (UserList,)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# if sets is still available, register the mutable set from there as well
|
|
||||||
try:
|
|
||||||
from sets import Set
|
|
||||||
_mutable_set_types += (Set,)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
#: register Python 2.6 abstract base classes
|
|
||||||
try:
|
|
||||||
from collections import MutableSet, MutableMapping, MutableSequence
|
|
||||||
_mutable_set_types += (MutableSet,)
|
|
||||||
_mutable_mapping_types += (MutableMapping,)
|
|
||||||
_mutable_sequence_types += (MutableSequence,)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
_mutable_spec = (
|
|
||||||
(_mutable_set_types, frozenset([
|
|
||||||
'add', 'clear', 'difference_update', 'discard', 'pop', 'remove',
|
|
||||||
'symmetric_difference_update', 'update'
|
|
||||||
])),
|
|
||||||
(_mutable_mapping_types, frozenset([
|
|
||||||
'clear', 'pop', 'popitem', 'setdefault', 'update'
|
|
||||||
])),
|
|
||||||
(_mutable_sequence_types, frozenset([
|
|
||||||
'append', 'reverse', 'insert', 'sort', 'extend', 'remove'
|
|
||||||
])),
|
|
||||||
(deque, frozenset([
|
|
||||||
'append', 'appendleft', 'clear', 'extend', 'extendleft', 'pop',
|
|
||||||
'popleft', 'remove', 'rotate'
|
|
||||||
]))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_range(*args):
|
|
||||||
"""A range that can't generate ranges with a length of more than
|
|
||||||
MAX_RANGE items.
|
|
||||||
"""
|
|
||||||
rng = xrange(*args)
|
|
||||||
if len(rng) > MAX_RANGE:
|
|
||||||
raise OverflowError('range too big, maximum size for range is %d' %
|
|
||||||
MAX_RANGE)
|
|
||||||
return rng
|
|
||||||
|
|
||||||
|
|
||||||
def unsafe(f):
|
|
||||||
"""
|
|
||||||
Mark a function or method as unsafe::
|
|
||||||
|
|
||||||
@unsafe
|
|
||||||
def delete(self):
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
f.unsafe_callable = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def is_internal_attribute(obj, attr):
|
|
||||||
"""Test if the attribute given is an internal python attribute. For
|
|
||||||
example this function returns `True` for the `func_code` attribute of
|
|
||||||
python objects. This is useful if the environment method
|
|
||||||
:meth:`~SandboxedEnvironment.is_safe_attribute` is overriden.
|
|
||||||
|
|
||||||
>>> from jinja2.sandbox import is_internal_attribute
|
|
||||||
>>> is_internal_attribute(lambda: None, "func_code")
|
|
||||||
True
|
|
||||||
>>> is_internal_attribute((lambda x:x).func_code, 'co_code')
|
|
||||||
True
|
|
||||||
>>> is_internal_attribute(str, "upper")
|
|
||||||
False
|
|
||||||
"""
|
|
||||||
if isinstance(obj, FunctionType):
|
|
||||||
if attr in UNSAFE_FUNCTION_ATTRIBUTES:
|
|
||||||
return True
|
|
||||||
elif isinstance(obj, MethodType):
|
|
||||||
if attr in UNSAFE_FUNCTION_ATTRIBUTES or \
|
|
||||||
attr in UNSAFE_METHOD_ATTRIBUTES:
|
|
||||||
return True
|
|
||||||
elif isinstance(obj, type):
|
|
||||||
if attr == 'mro':
|
|
||||||
return True
|
|
||||||
elif isinstance(obj, (CodeType, TracebackType, FrameType)):
|
|
||||||
return True
|
|
||||||
elif isinstance(obj, GeneratorType):
|
|
||||||
if attr == 'gi_frame':
|
|
||||||
return True
|
|
||||||
return attr.startswith('__')
|
|
||||||
|
|
||||||
|
|
||||||
def modifies_known_mutable(obj, attr):
|
|
||||||
"""This function checks if an attribute on a builtin mutable object
|
|
||||||
(list, dict, set or deque) would modify it if called. It also supports
|
|
||||||
the "user"-versions of the objects (`sets.Set`, `UserDict.*` etc.) and
|
|
||||||
with Python 2.6 onwards the abstract base classes `MutableSet`,
|
|
||||||
`MutableMapping`, and `MutableSequence`.
|
|
||||||
|
|
||||||
>>> modifies_known_mutable({}, "clear")
|
|
||||||
True
|
|
||||||
>>> modifies_known_mutable({}, "keys")
|
|
||||||
False
|
|
||||||
>>> modifies_known_mutable([], "append")
|
|
||||||
True
|
|
||||||
>>> modifies_known_mutable([], "index")
|
|
||||||
False
|
|
||||||
|
|
||||||
If called with an unsupported object (such as unicode) `False` is
|
|
||||||
returned.
|
|
||||||
|
|
||||||
>>> modifies_known_mutable("foo", "upper")
|
|
||||||
False
|
|
||||||
"""
|
|
||||||
for typespec, unsafe in _mutable_spec:
|
|
||||||
if isinstance(obj, typespec):
|
|
||||||
return attr in unsafe
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class SandboxedEnvironment(Environment):
|
|
||||||
"""The sandboxed environment. It works like the regular environment but
|
|
||||||
tells the compiler to generate sandboxed code. Additionally subclasses of
|
|
||||||
this environment may override the methods that tell the runtime what
|
|
||||||
attributes or functions are safe to access.
|
|
||||||
|
|
||||||
If the template tries to access insecure code a :exc:`SecurityError` is
|
|
||||||
raised. However also other exceptions may occour during the rendering so
|
|
||||||
the caller has to ensure that all exceptions are catched.
|
|
||||||
"""
|
|
||||||
sandboxed = True
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
Environment.__init__(self, *args, **kwargs)
|
|
||||||
self.globals['range'] = safe_range
|
|
||||||
|
|
||||||
def is_safe_attribute(self, obj, attr, value):
|
|
||||||
"""The sandboxed environment will call this method to check if the
|
|
||||||
attribute of an object is safe to access. Per default all attributes
|
|
||||||
starting with an underscore are considered private as well as the
|
|
||||||
special attributes of internal python objects as returned by the
|
|
||||||
:func:`is_internal_attribute` function.
|
|
||||||
"""
|
|
||||||
return not (attr.startswith('_') or is_internal_attribute(obj, attr))
|
|
||||||
|
|
||||||
def is_safe_callable(self, obj):
|
|
||||||
"""Check if an object is safely callable. Per default a function is
|
|
||||||
considered safe unless the `unsafe_callable` attribute exists and is
|
|
||||||
True. Override this method to alter the behavior, but this won't
|
|
||||||
affect the `unsafe` decorator from this module.
|
|
||||||
"""
|
|
||||||
return not (getattr(obj, 'unsafe_callable', False) or \
|
|
||||||
getattr(obj, 'alters_data', False))
|
|
||||||
|
|
||||||
def getitem(self, obj, argument):
|
|
||||||
"""Subscribe an object from sandboxed code."""
|
|
||||||
try:
|
|
||||||
return obj[argument]
|
|
||||||
except (TypeError, LookupError):
|
|
||||||
if isinstance(argument, basestring):
|
|
||||||
try:
|
|
||||||
attr = str(argument)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
value = getattr(obj, attr)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if self.is_safe_attribute(obj, argument, value):
|
|
||||||
return value
|
|
||||||
return self.unsafe_undefined(obj, argument)
|
|
||||||
return self.undefined(obj=obj, name=argument)
|
|
||||||
|
|
||||||
def getattr(self, obj, attribute):
|
|
||||||
"""Subscribe an object from sandboxed code and prefer the
|
|
||||||
attribute. The attribute passed *must* be a bytestring.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
value = getattr(obj, attribute)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
return obj[attribute]
|
|
||||||
except (TypeError, LookupError):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if self.is_safe_attribute(obj, attribute, value):
|
|
||||||
return value
|
|
||||||
return self.unsafe_undefined(obj, attribute)
|
|
||||||
return self.undefined(obj=obj, name=attribute)
|
|
||||||
|
|
||||||
def unsafe_undefined(self, obj, attribute):
|
|
||||||
"""Return an undefined object for unsafe attributes."""
|
|
||||||
return self.undefined('access to attribute %r of %r '
|
|
||||||
'object is unsafe.' % (
|
|
||||||
attribute,
|
|
||||||
obj.__class__.__name__
|
|
||||||
), name=attribute, obj=obj, exc=SecurityError)
|
|
||||||
|
|
||||||
def call(__self, __context, __obj, *args, **kwargs):
|
|
||||||
"""Call an object from sandboxed code."""
|
|
||||||
# the double prefixes are to avoid double keyword argument
|
|
||||||
# errors when proxying the call.
|
|
||||||
if not __self.is_safe_callable(__obj):
|
|
||||||
raise SecurityError('%r is not safely callable' % (__obj,))
|
|
||||||
return __context.call(__obj, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ImmutableSandboxedEnvironment(SandboxedEnvironment):
|
|
||||||
"""Works exactly like the regular `SandboxedEnvironment` but does not
|
|
||||||
permit modifications on the builtin mutable objects `list`, `set`, and
|
|
||||||
`dict` by using the :func:`modifies_known_mutable` function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def is_safe_attribute(self, obj, attr, value):
|
|
||||||
if not SandboxedEnvironment.is_safe_attribute(self, obj, attr, value):
|
|
||||||
return False
|
|
||||||
return not modifies_known_mutable(obj, attr)
|
|
|
@ -1,146 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.tests
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja test functions. Used with the "is" operator.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
from jinja2.runtime import Undefined
|
|
||||||
|
|
||||||
# nose, nothing here to test
|
|
||||||
__test__ = False
|
|
||||||
|
|
||||||
|
|
||||||
number_re = re.compile(r'^-?\d+(\.\d+)?$')
|
|
||||||
regex_type = type(number_re)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
test_callable = callable
|
|
||||||
except NameError:
|
|
||||||
def test_callable(x):
|
|
||||||
return hasattr(x, '__call__')
|
|
||||||
|
|
||||||
|
|
||||||
def test_odd(value):
|
|
||||||
"""Return true if the variable is odd."""
|
|
||||||
return value % 2 == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_even(value):
|
|
||||||
"""Return true if the variable is even."""
|
|
||||||
return value % 2 == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_divisibleby(value, num):
|
|
||||||
"""Check if a variable is divisible by a number."""
|
|
||||||
return value % num == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_defined(value):
|
|
||||||
"""Return true if the variable is defined:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% if variable is defined %}
|
|
||||||
value of variable: {{ variable }}
|
|
||||||
{% else %}
|
|
||||||
variable is not defined
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
See the :func:`default` filter for a simple way to set undefined
|
|
||||||
variables.
|
|
||||||
"""
|
|
||||||
return not isinstance(value, Undefined)
|
|
||||||
|
|
||||||
|
|
||||||
def test_undefined(value):
|
|
||||||
"""Like :func:`defined` but the other way round."""
|
|
||||||
return isinstance(value, Undefined)
|
|
||||||
|
|
||||||
|
|
||||||
def test_none(value):
|
|
||||||
"""Return true if the variable is none."""
|
|
||||||
return value is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_lower(value):
|
|
||||||
"""Return true if the variable is lowercased."""
|
|
||||||
return unicode(value).islower()
|
|
||||||
|
|
||||||
|
|
||||||
def test_upper(value):
|
|
||||||
"""Return true if the variable is uppercased."""
|
|
||||||
return unicode(value).isupper()
|
|
||||||
|
|
||||||
|
|
||||||
def test_string(value):
|
|
||||||
"""Return true if the object is a string."""
|
|
||||||
return isinstance(value, basestring)
|
|
||||||
|
|
||||||
|
|
||||||
def test_number(value):
|
|
||||||
"""Return true if the variable is a number."""
|
|
||||||
return isinstance(value, (int, long, float, complex))
|
|
||||||
|
|
||||||
|
|
||||||
def test_sequence(value):
|
|
||||||
"""Return true if the variable is a sequence. Sequences are variables
|
|
||||||
that are iterable.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
len(value)
|
|
||||||
value.__getitem__
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def test_sameas(value, other):
|
|
||||||
"""Check if an object points to the same memory address than another
|
|
||||||
object:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% if foo.attribute is sameas false %}
|
|
||||||
the foo attribute really is the `False` singleton
|
|
||||||
{% endif %}
|
|
||||||
"""
|
|
||||||
return value is other
|
|
||||||
|
|
||||||
|
|
||||||
def test_iterable(value):
|
|
||||||
"""Check if it's possible to iterate over an object."""
|
|
||||||
try:
|
|
||||||
iter(value)
|
|
||||||
except TypeError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def test_escaped(value):
|
|
||||||
"""Check if the value is escaped."""
|
|
||||||
return hasattr(value, '__html__')
|
|
||||||
|
|
||||||
|
|
||||||
TESTS = {
|
|
||||||
'odd': test_odd,
|
|
||||||
'even': test_even,
|
|
||||||
'divisibleby': test_divisibleby,
|
|
||||||
'defined': test_defined,
|
|
||||||
'undefined': test_undefined,
|
|
||||||
'none': test_none,
|
|
||||||
'lower': test_lower,
|
|
||||||
'upper': test_upper,
|
|
||||||
'string': test_string,
|
|
||||||
'number': test_number,
|
|
||||||
'sequence': test_sequence,
|
|
||||||
'iterable': test_iterable,
|
|
||||||
'callable': test_callable,
|
|
||||||
'sameas': test_sameas,
|
|
||||||
'escaped': test_escaped
|
|
||||||
}
|
|
|
@ -1,818 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.utils
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Utility functions.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import errno
|
|
||||||
try:
|
|
||||||
from thread import allocate_lock
|
|
||||||
except ImportError:
|
|
||||||
from dummy_thread import allocate_lock
|
|
||||||
from collections import deque
|
|
||||||
from itertools import imap
|
|
||||||
|
|
||||||
|
|
||||||
_word_split_re = re.compile(r'(\s+)')
|
|
||||||
_punctuation_re = re.compile(
|
|
||||||
'^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % (
|
|
||||||
'|'.join(imap(re.escape, ('(', '<', '<'))),
|
|
||||||
'|'.join(imap(re.escape, ('.', ',', ')', '>', '\n', '>')))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
|
|
||||||
_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
|
|
||||||
_entity_re = re.compile(r'&([^;]+);')
|
|
||||||
_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
||||||
_digits = '0123456789'
|
|
||||||
|
|
||||||
# special singleton representing missing values for the runtime
|
|
||||||
missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})()
|
|
||||||
|
|
||||||
# internal code
|
|
||||||
internal_code = set()
|
|
||||||
|
|
||||||
|
|
||||||
# concatenate a list of strings and convert them to unicode.
|
|
||||||
# unfortunately there is a bug in python 2.4 and lower that causes
|
|
||||||
# unicode.join trash the traceback.
|
|
||||||
_concat = u''.join
|
|
||||||
try:
|
|
||||||
def _test_gen_bug():
|
|
||||||
raise TypeError(_test_gen_bug)
|
|
||||||
yield None
|
|
||||||
_concat(_test_gen_bug())
|
|
||||||
except TypeError as _error:
|
|
||||||
if not _error.args or _error.args[0] is not _test_gen_bug:
|
|
||||||
def concat(gen):
|
|
||||||
try:
|
|
||||||
return _concat(list(gen))
|
|
||||||
except:
|
|
||||||
# this hack is needed so that the current frame
|
|
||||||
# does not show up in the traceback.
|
|
||||||
exc_type, exc_value, tb = sys.exc_info()
|
|
||||||
raise exc_type, exc_value, tb.tb_next
|
|
||||||
else:
|
|
||||||
concat = _concat
|
|
||||||
del _test_gen_bug, _error
|
|
||||||
|
|
||||||
|
|
||||||
# for python 2.x we create outselves a next() function that does the
|
|
||||||
# basics without exception catching.
|
|
||||||
try:
|
|
||||||
next = next
|
|
||||||
except NameError:
|
|
||||||
def next(x):
|
|
||||||
return x.next()
|
|
||||||
|
|
||||||
|
|
||||||
# if this python version is unable to deal with unicode filenames
|
|
||||||
# when passed to encode we let this function encode it properly.
|
|
||||||
# This is used in a couple of places. As far as Jinja is concerned
|
|
||||||
# filenames are unicode *or* bytestrings in 2.x and unicode only in
|
|
||||||
# 3.x because compile cannot handle bytes
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
def _encode_filename(filename):
|
|
||||||
if isinstance(filename, unicode):
|
|
||||||
return filename.encode('utf-8')
|
|
||||||
return filename
|
|
||||||
else:
|
|
||||||
def _encode_filename(filename):
|
|
||||||
assert filename is None or isinstance(filename, str), \
|
|
||||||
'filenames must be strings'
|
|
||||||
return filename
|
|
||||||
|
|
||||||
from keyword import iskeyword as is_python_keyword
|
|
||||||
|
|
||||||
|
|
||||||
# common types. These do exist in the special types module too which however
|
|
||||||
# does not exist in IronPython out of the box. Also that way we don't have
|
|
||||||
# to deal with implementation specific stuff here
|
|
||||||
class _C(object):
|
|
||||||
def method(self): pass
|
|
||||||
def _func():
|
|
||||||
yield None
|
|
||||||
FunctionType = type(_func)
|
|
||||||
GeneratorType = type(_func())
|
|
||||||
MethodType = type(_C.method)
|
|
||||||
CodeType = type(_C.method.func_code)
|
|
||||||
try:
|
|
||||||
raise TypeError()
|
|
||||||
except TypeError:
|
|
||||||
_tb = sys.exc_info()[2]
|
|
||||||
TracebackType = type(_tb)
|
|
||||||
FrameType = type(_tb.tb_frame)
|
|
||||||
del _C, _tb, _func
|
|
||||||
|
|
||||||
|
|
||||||
def contextfunction(f):
|
|
||||||
"""This decorator can be used to mark a function or method context callable.
|
|
||||||
A context callable is passed the active :class:`Context` as first argument when
|
|
||||||
called from the template. This is useful if a function wants to get access
|
|
||||||
to the context or functions provided on the context object. For example
|
|
||||||
a function that returns a sorted list of template variables the current
|
|
||||||
template exports could look like this::
|
|
||||||
|
|
||||||
@contextfunction
|
|
||||||
def get_exported_names(context):
|
|
||||||
return sorted(context.exported_vars)
|
|
||||||
"""
|
|
||||||
f.contextfunction = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def evalcontextfunction(f):
|
|
||||||
"""This decoraotr can be used to mark a function or method as an eval
|
|
||||||
context callable. This is similar to the :func:`contextfunction`
|
|
||||||
but instead of passing the context, an evaluation context object is
|
|
||||||
passed. For more information about the eval context, see
|
|
||||||
:ref:`eval-context`.
|
|
||||||
|
|
||||||
.. versionadded:: 2.4
|
|
||||||
"""
|
|
||||||
f.evalcontextfunction = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def environmentfunction(f):
|
|
||||||
"""This decorator can be used to mark a function or method as environment
|
|
||||||
callable. This decorator works exactly like the :func:`contextfunction`
|
|
||||||
decorator just that the first argument is the active :class:`Environment`
|
|
||||||
and not context.
|
|
||||||
"""
|
|
||||||
f.environmentfunction = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def internalcode(f):
|
|
||||||
"""Marks the function as internally used"""
|
|
||||||
internal_code.add(f.func_code)
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def is_undefined(obj):
|
|
||||||
"""Check if the object passed is undefined. This does nothing more than
|
|
||||||
performing an instance check against :class:`Undefined` but looks nicer.
|
|
||||||
This can be used for custom filters or tests that want to react to
|
|
||||||
undefined variables. For example a custom default filter can look like
|
|
||||||
this::
|
|
||||||
|
|
||||||
def default(var, default=''):
|
|
||||||
if is_undefined(var):
|
|
||||||
return default
|
|
||||||
return var
|
|
||||||
"""
|
|
||||||
from jinja2.runtime import Undefined
|
|
||||||
return isinstance(obj, Undefined)
|
|
||||||
|
|
||||||
|
|
||||||
def consume(iterable):
|
|
||||||
"""Consumes an iterable without doing anything with it."""
|
|
||||||
for event in iterable:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def clear_caches():
|
|
||||||
"""Jinja2 keeps internal caches for environments and lexers. These are
|
|
||||||
used so that Jinja2 doesn't have to recreate environments and lexers all
|
|
||||||
the time. Normally you don't have to care about that but if you are
|
|
||||||
messuring memory consumption you may want to clean the caches.
|
|
||||||
"""
|
|
||||||
from jinja2.environment import _spontaneous_environments
|
|
||||||
from jinja2.lexer import _lexer_cache
|
|
||||||
_spontaneous_environments.clear()
|
|
||||||
_lexer_cache.clear()
|
|
||||||
|
|
||||||
|
|
||||||
def import_string(import_name, silent=False):
|
|
||||||
"""Imports an object based on a string. This use useful if you want to
|
|
||||||
use import paths as endpoints or something similar. An import path can
|
|
||||||
be specified either in dotted notation (``xml.sax.saxutils.escape``)
|
|
||||||
or with a colon as object delimiter (``xml.sax.saxutils:escape``).
|
|
||||||
|
|
||||||
If the `silent` is True the return value will be `None` if the import
|
|
||||||
fails.
|
|
||||||
|
|
||||||
:return: imported object
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if ':' in import_name:
|
|
||||||
module, obj = import_name.split(':', 1)
|
|
||||||
elif '.' in import_name:
|
|
||||||
items = import_name.split('.')
|
|
||||||
module = '.'.join(items[:-1])
|
|
||||||
obj = items[-1]
|
|
||||||
else:
|
|
||||||
return __import__(import_name)
|
|
||||||
return getattr(__import__(module, None, None, [obj]), obj)
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
if not silent:
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def open_if_exists(filename, mode='rb'):
|
|
||||||
"""Returns a file descriptor for the filename if that file exists,
|
|
||||||
otherwise `None`.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return open(filename, mode)
|
|
||||||
except IOError, e:
|
|
||||||
if e.errno not in (errno.ENOENT, errno.EISDIR):
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def object_type_repr(obj):
|
|
||||||
"""Returns the name of the object's type. For some recognized
|
|
||||||
singletons the name of the object is returned instead. (For
|
|
||||||
example for `None` and `Ellipsis`).
|
|
||||||
"""
|
|
||||||
if obj is None:
|
|
||||||
return 'None'
|
|
||||||
elif obj is Ellipsis:
|
|
||||||
return 'Ellipsis'
|
|
||||||
if obj.__class__.__module__ == '__builtin__':
|
|
||||||
name = obj.__class__.__name__
|
|
||||||
else:
|
|
||||||
name = obj.__class__.__module__ + '.' + obj.__class__.__name__
|
|
||||||
return '%s object' % name
|
|
||||||
|
|
||||||
|
|
||||||
def pformat(obj, verbose=False):
|
|
||||||
"""Prettyprint an object. Either use the `pretty` library or the
|
|
||||||
builtin `pprint`.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from pretty import pretty
|
|
||||||
return pretty(obj, verbose=verbose)
|
|
||||||
except ImportError:
|
|
||||||
from pprint import pformat
|
|
||||||
return pformat(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def urlize(text, trim_url_limit=None, nofollow=False):
|
|
||||||
"""Converts any URLs in text into clickable links. Works on http://,
|
|
||||||
https:// and www. links. Links can have trailing punctuation (periods,
|
|
||||||
commas, close-parens) and leading punctuation (opening parens) and
|
|
||||||
it'll still do the right thing.
|
|
||||||
|
|
||||||
If trim_url_limit is not None, the URLs in link text will be limited
|
|
||||||
to trim_url_limit characters.
|
|
||||||
|
|
||||||
If nofollow is True, the URLs in link text will get a rel="nofollow"
|
|
||||||
attribute.
|
|
||||||
"""
|
|
||||||
trim_url = lambda x, limit=trim_url_limit: limit is not None \
|
|
||||||
and (x[:limit] + (len(x) >=limit and '...'
|
|
||||||
or '')) or x
|
|
||||||
words = _word_split_re.split(unicode(escape(text)))
|
|
||||||
nofollow_attr = nofollow and ' rel="nofollow"' or ''
|
|
||||||
for i, word in enumerate(words):
|
|
||||||
match = _punctuation_re.match(word)
|
|
||||||
if match:
|
|
||||||
lead, middle, trail = match.groups()
|
|
||||||
if middle.startswith('www.') or (
|
|
||||||
'@' not in middle and
|
|
||||||
not middle.startswith('http://') and
|
|
||||||
len(middle) > 0 and
|
|
||||||
middle[0] in _letters + _digits and (
|
|
||||||
middle.endswith('.org') or
|
|
||||||
middle.endswith('.net') or
|
|
||||||
middle.endswith('.com')
|
|
||||||
)):
|
|
||||||
middle = '<a href="http://%s"%s>%s</a>' % (middle,
|
|
||||||
nofollow_attr, trim_url(middle))
|
|
||||||
if middle.startswith('http://') or \
|
|
||||||
middle.startswith('https://'):
|
|
||||||
middle = '<a href="%s"%s>%s</a>' % (middle,
|
|
||||||
nofollow_attr, trim_url(middle))
|
|
||||||
if '@' in middle and not middle.startswith('www.') and \
|
|
||||||
not ':' in middle and _simple_email_re.match(middle):
|
|
||||||
middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
|
|
||||||
if lead + middle + trail != word:
|
|
||||||
words[i] = lead + middle + trail
|
|
||||||
return u''.join(words)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
|
|
||||||
"""Generate some lorem impsum for the template."""
|
|
||||||
from jinja2.constants import LOREM_IPSUM_WORDS
|
|
||||||
from random import choice, randrange
|
|
||||||
words = LOREM_IPSUM_WORDS.split()
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for _ in xrange(n):
|
|
||||||
next_capitalized = True
|
|
||||||
last_comma = last_fullstop = 0
|
|
||||||
word = None
|
|
||||||
last = None
|
|
||||||
p = []
|
|
||||||
|
|
||||||
# each paragraph contains out of 20 to 100 words.
|
|
||||||
for idx, _ in enumerate(xrange(randrange(min, max))):
|
|
||||||
while True:
|
|
||||||
word = choice(words)
|
|
||||||
if word != last:
|
|
||||||
last = word
|
|
||||||
break
|
|
||||||
if next_capitalized:
|
|
||||||
word = word.capitalize()
|
|
||||||
next_capitalized = False
|
|
||||||
# add commas
|
|
||||||
if idx - randrange(3, 8) > last_comma:
|
|
||||||
last_comma = idx
|
|
||||||
last_fullstop += 2
|
|
||||||
word += ','
|
|
||||||
# add end of sentences
|
|
||||||
if idx - randrange(10, 20) > last_fullstop:
|
|
||||||
last_comma = last_fullstop = idx
|
|
||||||
word += '.'
|
|
||||||
next_capitalized = True
|
|
||||||
p.append(word)
|
|
||||||
|
|
||||||
# ensure that the paragraph ends with a dot.
|
|
||||||
p = u' '.join(p)
|
|
||||||
if p.endswith(','):
|
|
||||||
p = p[:-1] + '.'
|
|
||||||
elif not p.endswith('.'):
|
|
||||||
p += '.'
|
|
||||||
result.append(p)
|
|
||||||
|
|
||||||
if not html:
|
|
||||||
return u'\n\n'.join(result)
|
|
||||||
return Markup(u'\n'.join(u'<p>%s</p>' % escape(x) for x in result))
|
|
||||||
|
|
||||||
|
|
||||||
class Markup(unicode):
|
|
||||||
r"""Marks a string as being safe for inclusion in HTML/XML output without
|
|
||||||
needing to be escaped. This implements the `__html__` interface a couple
|
|
||||||
of frameworks and web applications use. :class:`Markup` is a direct
|
|
||||||
subclass of `unicode` and provides all the methods of `unicode` just that
|
|
||||||
it escapes arguments passed and always returns `Markup`.
|
|
||||||
|
|
||||||
The `escape` function returns markup objects so that double escaping can't
|
|
||||||
happen. If you want to use autoescaping in Jinja just enable the
|
|
||||||
autoescaping feature in the environment.
|
|
||||||
|
|
||||||
The constructor of the :class:`Markup` class can be used for three
|
|
||||||
different things: When passed an unicode object it's assumed to be safe,
|
|
||||||
when passed an object with an HTML representation (has an `__html__`
|
|
||||||
method) that representation is used, otherwise the object passed is
|
|
||||||
converted into a unicode string and then assumed to be safe:
|
|
||||||
|
|
||||||
>>> Markup("Hello <em>World</em>!")
|
|
||||||
Markup(u'Hello <em>World</em>!')
|
|
||||||
>>> class Foo(object):
|
|
||||||
... def __html__(self):
|
|
||||||
... return '<a href="#">foo</a>'
|
|
||||||
...
|
|
||||||
>>> Markup(Foo())
|
|
||||||
Markup(u'<a href="#">foo</a>')
|
|
||||||
|
|
||||||
If you want object passed being always treated as unsafe you can use the
|
|
||||||
:meth:`escape` classmethod to create a :class:`Markup` object:
|
|
||||||
|
|
||||||
>>> Markup.escape("Hello <em>World</em>!")
|
|
||||||
Markup(u'Hello <em>World</em>!')
|
|
||||||
|
|
||||||
Operations on a markup string are markup aware which means that all
|
|
||||||
arguments are passed through the :func:`escape` function:
|
|
||||||
|
|
||||||
>>> em = Markup("<em>%s</em>")
|
|
||||||
>>> em % "foo & bar"
|
|
||||||
Markup(u'<em>foo & bar</em>')
|
|
||||||
>>> strong = Markup("<strong>%(text)s</strong>")
|
|
||||||
>>> strong % {'text': '<blink>hacker here</blink>'}
|
|
||||||
Markup(u'<strong><blink>hacker here</blink></strong>')
|
|
||||||
>>> Markup("<em>Hello</em> ") + "<foo>"
|
|
||||||
Markup(u'<em>Hello</em> <foo>')
|
|
||||||
"""
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __new__(cls, base=u'', encoding=None, errors='strict'):
|
|
||||||
if hasattr(base, '__html__'):
|
|
||||||
base = base.__html__()
|
|
||||||
if encoding is None:
|
|
||||||
return unicode.__new__(cls, base)
|
|
||||||
return unicode.__new__(cls, base, encoding, errors)
|
|
||||||
|
|
||||||
def __html__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
if hasattr(other, '__html__') or isinstance(other, basestring):
|
|
||||||
return self.__class__(unicode(self) + unicode(escape(other)))
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __radd__(self, other):
|
|
||||||
if hasattr(other, '__html__') or isinstance(other, basestring):
|
|
||||||
return self.__class__(unicode(escape(other)) + unicode(self))
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __mul__(self, num):
|
|
||||||
if isinstance(num, (int, long)):
|
|
||||||
return self.__class__(unicode.__mul__(self, num))
|
|
||||||
return NotImplemented
|
|
||||||
__rmul__ = __mul__
|
|
||||||
|
|
||||||
def __mod__(self, arg):
|
|
||||||
if isinstance(arg, tuple):
|
|
||||||
arg = tuple(imap(_MarkupEscapeHelper, arg))
|
|
||||||
else:
|
|
||||||
arg = _MarkupEscapeHelper(arg)
|
|
||||||
return self.__class__(unicode.__mod__(self, arg))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s(%s)' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
unicode.__repr__(self)
|
|
||||||
)
|
|
||||||
|
|
||||||
def join(self, seq):
|
|
||||||
return self.__class__(unicode.join(self, imap(escape, seq)))
|
|
||||||
join.__doc__ = unicode.join.__doc__
|
|
||||||
|
|
||||||
def split(self, *args, **kwargs):
|
|
||||||
return map(self.__class__, unicode.split(self, *args, **kwargs))
|
|
||||||
split.__doc__ = unicode.split.__doc__
|
|
||||||
|
|
||||||
def rsplit(self, *args, **kwargs):
|
|
||||||
return map(self.__class__, unicode.rsplit(self, *args, **kwargs))
|
|
||||||
rsplit.__doc__ = unicode.rsplit.__doc__
|
|
||||||
|
|
||||||
def splitlines(self, *args, **kwargs):
|
|
||||||
return map(self.__class__, unicode.splitlines(self, *args, **kwargs))
|
|
||||||
splitlines.__doc__ = unicode.splitlines.__doc__
|
|
||||||
|
|
||||||
def unescape(self):
|
|
||||||
r"""Unescape markup again into an unicode string. This also resolves
|
|
||||||
known HTML4 and XHTML entities:
|
|
||||||
|
|
||||||
>>> Markup("Main » <em>About</em>").unescape()
|
|
||||||
u'Main \xbb <em>About</em>'
|
|
||||||
"""
|
|
||||||
from jinja2.constants import HTML_ENTITIES
|
|
||||||
def handle_match(m):
|
|
||||||
name = m.group(1)
|
|
||||||
if name in HTML_ENTITIES:
|
|
||||||
return unichr(HTML_ENTITIES[name])
|
|
||||||
try:
|
|
||||||
if name[:2] in ('#x', '#X'):
|
|
||||||
return unichr(int(name[2:], 16))
|
|
||||||
elif name.startswith('#'):
|
|
||||||
return unichr(int(name[1:]))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return u''
|
|
||||||
return _entity_re.sub(handle_match, unicode(self))
|
|
||||||
|
|
||||||
def striptags(self):
|
|
||||||
r"""Unescape markup into an unicode string and strip all tags. This
|
|
||||||
also resolves known HTML4 and XHTML entities. Whitespace is
|
|
||||||
normalized to one:
|
|
||||||
|
|
||||||
>>> Markup("Main » <em>About</em>").striptags()
|
|
||||||
u'Main \xbb About'
|
|
||||||
"""
|
|
||||||
stripped = u' '.join(_striptags_re.sub('', self).split())
|
|
||||||
return Markup(stripped).unescape()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def escape(cls, s):
|
|
||||||
"""Escape the string. Works like :func:`escape` with the difference
|
|
||||||
that for subclasses of :class:`Markup` this function would return the
|
|
||||||
correct subclass.
|
|
||||||
"""
|
|
||||||
rv = escape(s)
|
|
||||||
if rv.__class__ is not cls:
|
|
||||||
return cls(rv)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def make_wrapper(name):
|
|
||||||
orig = getattr(unicode, name)
|
|
||||||
def func(self, *args, **kwargs):
|
|
||||||
args = _escape_argspec(list(args), enumerate(args))
|
|
||||||
_escape_argspec(kwargs, kwargs.iteritems())
|
|
||||||
return self.__class__(orig(self, *args, **kwargs))
|
|
||||||
func.__name__ = orig.__name__
|
|
||||||
func.__doc__ = orig.__doc__
|
|
||||||
return func
|
|
||||||
|
|
||||||
for method in '__getitem__', 'capitalize', \
|
|
||||||
'title', 'lower', 'upper', 'replace', 'ljust', \
|
|
||||||
'rjust', 'lstrip', 'rstrip', 'center', 'strip', \
|
|
||||||
'translate', 'expandtabs', 'swapcase', 'zfill':
|
|
||||||
locals()[method] = make_wrapper(method)
|
|
||||||
|
|
||||||
# new in python 2.5
|
|
||||||
if hasattr(unicode, 'partition'):
|
|
||||||
partition = make_wrapper('partition'),
|
|
||||||
rpartition = make_wrapper('rpartition')
|
|
||||||
|
|
||||||
# new in python 2.6
|
|
||||||
if hasattr(unicode, 'format'):
|
|
||||||
format = make_wrapper('format')
|
|
||||||
|
|
||||||
# not in python 3
|
|
||||||
if hasattr(unicode, '__getslice__'):
|
|
||||||
__getslice__ = make_wrapper('__getslice__')
|
|
||||||
|
|
||||||
del method, make_wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_argspec(obj, iterable):
|
|
||||||
"""Helper for various string-wrapped functions."""
|
|
||||||
for key, value in iterable:
|
|
||||||
if hasattr(value, '__html__') or isinstance(value, basestring):
|
|
||||||
obj[key] = escape(value)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class _MarkupEscapeHelper(object):
|
|
||||||
"""Helper for Markup.__mod__"""
|
|
||||||
|
|
||||||
def __init__(self, obj):
|
|
||||||
self.obj = obj
|
|
||||||
|
|
||||||
__getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x])
|
|
||||||
__str__ = lambda s: str(escape(s.obj))
|
|
||||||
__unicode__ = lambda s: unicode(escape(s.obj))
|
|
||||||
__repr__ = lambda s: str(escape(repr(s.obj)))
|
|
||||||
__int__ = lambda s: int(s.obj)
|
|
||||||
__float__ = lambda s: float(s.obj)
|
|
||||||
|
|
||||||
|
|
||||||
class LRUCache(object):
|
|
||||||
"""A simple LRU Cache implementation."""
|
|
||||||
|
|
||||||
# this is fast for small capacities (something below 1000) but doesn't
|
|
||||||
# scale. But as long as it's only used as storage for templates this
|
|
||||||
# won't do any harm.
|
|
||||||
|
|
||||||
def __init__(self, capacity):
|
|
||||||
self.capacity = capacity
|
|
||||||
self._mapping = {}
|
|
||||||
self._queue = deque()
|
|
||||||
self._postinit()
|
|
||||||
|
|
||||||
def _postinit(self):
|
|
||||||
# alias all queue methods for faster lookup
|
|
||||||
self._popleft = self._queue.popleft
|
|
||||||
self._pop = self._queue.pop
|
|
||||||
if hasattr(self._queue, 'remove'):
|
|
||||||
self._remove = self._queue.remove
|
|
||||||
self._wlock = allocate_lock()
|
|
||||||
self._append = self._queue.append
|
|
||||||
|
|
||||||
def _remove(self, obj):
|
|
||||||
"""Python 2.4 compatibility."""
|
|
||||||
for idx, item in enumerate(self._queue):
|
|
||||||
if item == obj:
|
|
||||||
del self._queue[idx]
|
|
||||||
break
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return {
|
|
||||||
'capacity': self.capacity,
|
|
||||||
'_mapping': self._mapping,
|
|
||||||
'_queue': self._queue
|
|
||||||
}
|
|
||||||
|
|
||||||
def __setstate__(self, d):
|
|
||||||
self.__dict__.update(d)
|
|
||||||
self._postinit()
|
|
||||||
|
|
||||||
def __getnewargs__(self):
|
|
||||||
return (self.capacity,)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
"""Return an shallow copy of the instance."""
|
|
||||||
rv = self.__class__(self.capacity)
|
|
||||||
rv._mapping.update(self._mapping)
|
|
||||||
rv._queue = deque(self._queue)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
"""Return an item from the cache dict or `default`"""
|
|
||||||
try:
|
|
||||||
return self[key]
|
|
||||||
except KeyError:
|
|
||||||
return default
|
|
||||||
|
|
||||||
def setdefault(self, key, default=None):
|
|
||||||
"""Set `default` if the key is not in the cache otherwise
|
|
||||||
leave unchanged. Return the value of this key.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self[key]
|
|
||||||
except KeyError:
|
|
||||||
self[key] = default
|
|
||||||
return default
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Clear the cache."""
|
|
||||||
self._wlock.acquire()
|
|
||||||
try:
|
|
||||||
self._mapping.clear()
|
|
||||||
self._queue.clear()
|
|
||||||
finally:
|
|
||||||
self._wlock.release()
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
"""Check if a key exists in this cache."""
|
|
||||||
return key in self._mapping
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
"""Return the current size of the cache."""
|
|
||||||
return len(self._mapping)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s %r>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self._mapping
|
|
||||||
)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
"""Get an item from the cache. Moves the item up so that it has the
|
|
||||||
highest priority then.
|
|
||||||
|
|
||||||
Raise an `KeyError` if it does not exist.
|
|
||||||
"""
|
|
||||||
rv = self._mapping[key]
|
|
||||||
if self._queue[-1] != key:
|
|
||||||
try:
|
|
||||||
self._remove(key)
|
|
||||||
except ValueError:
|
|
||||||
# if something removed the key from the container
|
|
||||||
# when we read, ignore the ValueError that we would
|
|
||||||
# get otherwise.
|
|
||||||
pass
|
|
||||||
self._append(key)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
"""Sets the value for an item. Moves the item up so that it
|
|
||||||
has the highest priority then.
|
|
||||||
"""
|
|
||||||
self._wlock.acquire()
|
|
||||||
try:
|
|
||||||
if key in self._mapping:
|
|
||||||
try:
|
|
||||||
self._remove(key)
|
|
||||||
except ValueError:
|
|
||||||
# __getitem__ is not locked, it might happen
|
|
||||||
pass
|
|
||||||
elif len(self._mapping) == self.capacity:
|
|
||||||
del self._mapping[self._popleft()]
|
|
||||||
self._append(key)
|
|
||||||
self._mapping[key] = value
|
|
||||||
finally:
|
|
||||||
self._wlock.release()
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
"""Remove an item from the cache dict.
|
|
||||||
Raise an `KeyError` if it does not exist.
|
|
||||||
"""
|
|
||||||
self._wlock.acquire()
|
|
||||||
try:
|
|
||||||
del self._mapping[key]
|
|
||||||
try:
|
|
||||||
self._remove(key)
|
|
||||||
except ValueError:
|
|
||||||
# __getitem__ is not locked, it might happen
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self._wlock.release()
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
"""Return a list of items."""
|
|
||||||
result = [(key, self._mapping[key]) for key in list(self._queue)]
|
|
||||||
result.reverse()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def iteritems(self):
|
|
||||||
"""Iterate over all items."""
|
|
||||||
return iter(self.items())
|
|
||||||
|
|
||||||
def values(self):
|
|
||||||
"""Return a list of all values."""
|
|
||||||
return [x[1] for x in self.items()]
|
|
||||||
|
|
||||||
def itervalue(self):
|
|
||||||
"""Iterate over all values."""
|
|
||||||
return iter(self.values())
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
"""Return a list of all keys ordered by most recent usage."""
|
|
||||||
return list(self)
|
|
||||||
|
|
||||||
def iterkeys(self):
|
|
||||||
"""Iterate over all keys in the cache dict, ordered by
|
|
||||||
the most recent usage.
|
|
||||||
"""
|
|
||||||
return reversed(tuple(self._queue))
|
|
||||||
|
|
||||||
__iter__ = iterkeys
|
|
||||||
|
|
||||||
def __reversed__(self):
|
|
||||||
"""Iterate over the values in the cache dict, oldest items
|
|
||||||
coming first.
|
|
||||||
"""
|
|
||||||
return iter(tuple(self._queue))
|
|
||||||
|
|
||||||
__copy__ = copy
|
|
||||||
|
|
||||||
|
|
||||||
# register the LRU cache as mutable mapping if possible
|
|
||||||
try:
|
|
||||||
from collections import MutableMapping
|
|
||||||
MutableMapping.register(LRUCache)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Cycler(object):
|
|
||||||
"""A cycle helper for templates."""
|
|
||||||
|
|
||||||
def __init__(self, *items):
|
|
||||||
if not items:
|
|
||||||
raise RuntimeError('at least one item has to be provided')
|
|
||||||
self.items = items
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Resets the cycle."""
|
|
||||||
self.pos = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current(self):
|
|
||||||
"""Returns the current item."""
|
|
||||||
return self.items[self.pos]
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
"""Goes one item ahead and returns it."""
|
|
||||||
rv = self.current
|
|
||||||
self.pos = (self.pos + 1) % len(self.items)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class Joiner(object):
|
|
||||||
"""A joining helper for templates."""
|
|
||||||
|
|
||||||
def __init__(self, sep=u', '):
|
|
||||||
self.sep = sep
|
|
||||||
self.used = False
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
if not self.used:
|
|
||||||
self.used = True
|
|
||||||
return u''
|
|
||||||
return self.sep
|
|
||||||
|
|
||||||
|
|
||||||
# we have to import it down here as the speedups module imports the
|
|
||||||
# markup type which is define above.
|
|
||||||
try:
|
|
||||||
from jinja2._speedups import escape, soft_unicode
|
|
||||||
except ImportError:
|
|
||||||
def escape(s):
|
|
||||||
"""Convert the characters &, <, >, ' and " in string s to HTML-safe
|
|
||||||
sequences. Use this if you need to display text that might contain
|
|
||||||
such characters in HTML. Marks return value as markup string.
|
|
||||||
"""
|
|
||||||
if hasattr(s, '__html__'):
|
|
||||||
return s.__html__()
|
|
||||||
return Markup(unicode(s)
|
|
||||||
.replace('&', '&')
|
|
||||||
.replace('>', '>')
|
|
||||||
.replace('<', '<')
|
|
||||||
.replace("'", ''')
|
|
||||||
.replace('"', '"')
|
|
||||||
)
|
|
||||||
|
|
||||||
def soft_unicode(s):
|
|
||||||
"""Make a string unicode if it isn't already. That way a markup
|
|
||||||
string is not converted back to unicode.
|
|
||||||
"""
|
|
||||||
if not isinstance(s, unicode):
|
|
||||||
s = unicode(s)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
# partials
|
|
||||||
try:
|
|
||||||
from functools import partial
|
|
||||||
except ImportError:
|
|
||||||
class partial(object):
|
|
||||||
def __init__(self, _func, *args, **kwargs):
|
|
||||||
self._func = _func
|
|
||||||
self._args = args
|
|
||||||
self._kwargs = kwargs
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
kwargs.update(self._kwargs)
|
|
||||||
return self._func(*(self._args + args), **kwargs)
|
|
|
@ -1,87 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja2.visitor
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements a visitor for the nodes.
|
|
||||||
|
|
||||||
:copyright: (c) 2010 by the Jinja Team.
|
|
||||||
:license: BSD.
|
|
||||||
"""
|
|
||||||
from jinja2.nodes import Node
|
|
||||||
|
|
||||||
|
|
||||||
class NodeVisitor(object):
|
|
||||||
"""Walks the abstract syntax tree and call visitor functions for every
|
|
||||||
node found. The visitor functions may return values which will be
|
|
||||||
forwarded by the `visit` method.
|
|
||||||
|
|
||||||
Per default the visitor functions for the nodes are ``'visit_'`` +
|
|
||||||
class name of the node. So a `TryFinally` node visit function would
|
|
||||||
be `visit_TryFinally`. This behavior can be changed by overriding
|
|
||||||
the `get_visitor` function. If no visitor function exists for a node
|
|
||||||
(return value `None`) the `generic_visit` visitor is used instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_visitor(self, node):
|
|
||||||
"""Return the visitor function for this node or `None` if no visitor
|
|
||||||
exists for this node. In that case the generic visit function is
|
|
||||||
used instead.
|
|
||||||
"""
|
|
||||||
method = 'visit_' + node.__class__.__name__
|
|
||||||
return getattr(self, method, None)
|
|
||||||
|
|
||||||
def visit(self, node, *args, **kwargs):
|
|
||||||
"""Visit a node."""
|
|
||||||
f = self.get_visitor(node)
|
|
||||||
if f is not None:
|
|
||||||
return f(node, *args, **kwargs)
|
|
||||||
return self.generic_visit(node, *args, **kwargs)
|
|
||||||
|
|
||||||
def generic_visit(self, node, *args, **kwargs):
|
|
||||||
"""Called if no explicit visitor function exists for a node."""
|
|
||||||
for node in node.iter_child_nodes():
|
|
||||||
self.visit(node, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class NodeTransformer(NodeVisitor):
|
|
||||||
"""Walks the abstract syntax tree and allows modifications of nodes.
|
|
||||||
|
|
||||||
The `NodeTransformer` will walk the AST and use the return value of the
|
|
||||||
visitor functions to replace or remove the old node. If the return
|
|
||||||
value of the visitor function is `None` the node will be removed
|
|
||||||
from the previous location otherwise it's replaced with the return
|
|
||||||
value. The return value may be the original node in which case no
|
|
||||||
replacement takes place.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def generic_visit(self, node, *args, **kwargs):
|
|
||||||
for field, old_value in node.iter_fields():
|
|
||||||
if isinstance(old_value, list):
|
|
||||||
new_values = []
|
|
||||||
for value in old_value:
|
|
||||||
if isinstance(value, Node):
|
|
||||||
value = self.visit(value, *args, **kwargs)
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
elif not isinstance(value, Node):
|
|
||||||
new_values.extend(value)
|
|
||||||
continue
|
|
||||||
new_values.append(value)
|
|
||||||
old_value[:] = new_values
|
|
||||||
elif isinstance(old_value, Node):
|
|
||||||
new_node = self.visit(old_value, *args, **kwargs)
|
|
||||||
if new_node is None:
|
|
||||||
delattr(node, field)
|
|
||||||
else:
|
|
||||||
setattr(node, field, new_node)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def visit_list(self, node, *args, **kwargs):
|
|
||||||
"""As transformers may return lists in some places this method
|
|
||||||
can be used to enforce a list as return value.
|
|
||||||
"""
|
|
||||||
rv = self.visit(node, *args, **kwargs)
|
|
||||||
if not isinstance(rv, list):
|
|
||||||
rv = [rv]
|
|
||||||
return rv
|
|
|
@ -1,40 +0,0 @@
|
||||||
|
|
||||||
# The namespace in Java file, with dots replaced with underscores
|
|
||||||
SDL_JAVA_PACKAGE_PATH := org_renpy_android
|
|
||||||
|
|
||||||
# Path to shared libraries - Android 1.6 cannot load them properly, thus we have to specify absolute path here
|
|
||||||
# SDL_SHARED_LIBRARIES_PATH := /data/data/de.schwardtnet.alienblaster/lib
|
|
||||||
|
|
||||||
# Path to files with application data - they should be downloaded from Internet on first app run inside
|
|
||||||
# Java sources, or unpacked from resources (TODO)
|
|
||||||
# Typically /sdcard/alienblaster
|
|
||||||
# Or /data/data/de.schwardtnet.alienblaster/files if you're planning to unpack data in application private folder
|
|
||||||
# Your application will just set current directory there
|
|
||||||
SDL_CURDIR_PATH := org.renpy.android
|
|
||||||
|
|
||||||
# Android Dev Phone G1 has trackball instead of cursor keys, and
|
|
||||||
# sends trackball movement events as rapid KeyDown/KeyUp events,
|
|
||||||
# this will make Up/Down/Left/Right key up events with X frames delay,
|
|
||||||
# so if application expects you to press and hold button it will process the event correctly.
|
|
||||||
# TODO: create a libsdl config file for that option and for key mapping/on-screen keyboard
|
|
||||||
SDL_TRACKBALL_KEYUP_DELAY := 1
|
|
||||||
|
|
||||||
# If the application designed for higher screen resolution enable this to get the screen
|
|
||||||
# resized in HW-accelerated way, however it eats a tiny bit of CPU
|
|
||||||
SDL_VIDEO_RENDER_RESIZE := 0
|
|
||||||
|
|
||||||
COMPILED_LIBRARIES := sdl_ttf sdl_image sdl_mixer
|
|
||||||
|
|
||||||
APPLICATION_ADDITIONAL_CFLAGS := -finline-functions -O2 -DSDL_JAVA_PACKAGE_PATH=$(SDL_JAVA_PACKAGE_PATH)
|
|
||||||
|
|
||||||
APPLICATION_ADDITIONAL_LDFLAGS := -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions
|
|
||||||
|
|
||||||
SDL_ADDITIONAL_CFLAGS := -DSDL_ANDROID_KEYCODE_MOUSE=UNKNOWN -DSDL_ANDROID_KEYCODE_0=SPACE -DSDL_ANDROID_KEYCODE_1=RETURN -DSDL_ANDROID_KEYCODE_2=LCTRL -DSDL_ANDROID_KEYCODE_3=LALT -DSDL_ANDROID_KEYCODE_4=RETURN
|
|
||||||
|
|
||||||
# If SDL_Mixer should link to libMAD
|
|
||||||
SDL_MIXER_USE_LIBMAD :=
|
|
||||||
ifneq ($(strip $(filter mad, $(COMPILED_LIBRARIES))),)
|
|
||||||
SDL_MIXER_USE_LIBMAD := 1
|
|
||||||
endif
|
|
||||||
|
|
||||||
include $(call all-subdir-makefiles)
|
|
|
@ -1,15 +0,0 @@
|
||||||
APP_PROJECT_PATH := $(call my-dir)/..
|
|
||||||
|
|
||||||
# Available libraries: mad sdl_mixer sdl_image sdl_ttf sdl_net sdl_blitpool sdl_gfx intl
|
|
||||||
# sdl_mixer depends on tremor and optionally mad
|
|
||||||
# sdl_image depends on png and jpeg
|
|
||||||
# sdl_ttf depends on freetype
|
|
||||||
|
|
||||||
APP_MODULES := application sdl sdl_main tremor png jpeg freetype sdl_ttf sdl_image sqlite3
|
|
||||||
|
|
||||||
APP_ABI := $(ARCH)
|
|
||||||
# AND: I have no idea why I have to specify app_platform when distribute.sh seems to just set the sysroot cflag
|
|
||||||
# AND: Either way, this has to *at least* be configurable
|
|
||||||
APP_PLATFORM := android-14
|
|
||||||
APP_STL := gnustl_static
|
|
||||||
APP_CFLAGS += $(OFLAG)
|
|
|
@ -1,68 +0,0 @@
|
||||||
LOCAL_PATH := $(call my-dir)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
|
|
||||||
LOCAL_MODULE := application
|
|
||||||
|
|
||||||
APP_SUBDIRS := $(patsubst $(LOCAL_PATH)/%, %, $(shell find $(LOCAL_PATH)/src/ -type d))
|
|
||||||
|
|
||||||
LOCAL_CFLAGS := $(foreach D, $(APP_SUBDIRS), -I$(LOCAL_PATH)/$(D)) \
|
|
||||||
-I$(LOCAL_PATH)/../sdl/include \
|
|
||||||
-I$(LOCAL_PATH)/../sdl_mixer \
|
|
||||||
-I$(LOCAL_PATH)/../sdl_image \
|
|
||||||
-I$(LOCAL_PATH)/../sdl_ttf \
|
|
||||||
-I$(LOCAL_PATH)/../sdl_net \
|
|
||||||
-I$(LOCAL_PATH)/../sdl_blitpool \
|
|
||||||
-I$(LOCAL_PATH)/../sdl_gfx \
|
|
||||||
-I$(LOCAL_PATH)/../png \
|
|
||||||
-I$(LOCAL_PATH)/../jpeg \
|
|
||||||
-I$(LOCAL_PATH)/../intl \
|
|
||||||
-I$(LOCAL_PATH)/.. \
|
|
||||||
-I$(LOCAL_PATH)/../../../../other_builds/$(PYTHON2_NAME)/$(ARCH)/python2/python-install/include/python2.7
|
|
||||||
# -I$(LOCAL_PATH)/../../../../python-install/include/python2.7
|
|
||||||
# -I$(LOCAL_PATH)/../../../build/python-install/include/python2.7
|
|
||||||
|
|
||||||
|
|
||||||
LOCAL_CFLAGS += $(APPLICATION_ADDITIONAL_CFLAGS)
|
|
||||||
|
|
||||||
#Change C++ file extension as appropriate
|
|
||||||
LOCAL_CPP_EXTENSION := .cpp
|
|
||||||
|
|
||||||
LOCAL_SRC_FILES := $(foreach F, $(APP_SUBDIRS), $(addprefix $(F)/,$(notdir $(wildcard $(LOCAL_PATH)/$(F)/*.cpp))))
|
|
||||||
# Uncomment to also add C sources
|
|
||||||
LOCAL_SRC_FILES += $(foreach F, $(APP_SUBDIRS), $(addprefix $(F)/,$(notdir $(wildcard $(LOCAL_PATH)/$(F)/*.c))))
|
|
||||||
|
|
||||||
LOCAL_SHARED_LIBRARIES := sdl $(COMPILED_LIBRARIES)
|
|
||||||
|
|
||||||
LOCAL_STATIC_LIBRARIES := jpeg png
|
|
||||||
|
|
||||||
LOCAL_LDLIBS := -lpython2.7 -lGLESv1_CM -ldl -llog -lz
|
|
||||||
|
|
||||||
# AND: Another hardcoded path that should be templated
|
|
||||||
# AND: NOT TEMPALTED! We can use $ARCH
|
|
||||||
LOCAL_LDFLAGS += -L$(LOCAL_PATH)/../../../../other_builds/$(PYTHON2_NAME)/$(ARCH)/python2/python-install/lib $(APPLICATION_ADDITIONAL_LDFLAGS)
|
|
||||||
|
|
||||||
LIBS_WITH_LONG_SYMBOLS := $(strip $(shell \
|
|
||||||
for f in $(LOCAL_PATH)/../../libs/$ARCH/*.so ; do \
|
|
||||||
if echo $$f | grep "libapplication[.]so" > /dev/null ; then \
|
|
||||||
continue ; \
|
|
||||||
fi ; \
|
|
||||||
if [ -e "$$f" ] ; then \
|
|
||||||
if nm -g $$f | cut -c 12- | egrep '.{128}' > /dev/null ; then \
|
|
||||||
echo $$f | grep -o 'lib[^/]*[.]so' ; \
|
|
||||||
fi ; \
|
|
||||||
fi ; \
|
|
||||||
done \
|
|
||||||
) )
|
|
||||||
|
|
||||||
ifneq "$(LIBS_WITH_LONG_SYMBOLS)" ""
|
|
||||||
$(foreach F, $(LIBS_WITH_LONG_SYMBOLS), \
|
|
||||||
$(info Library $(F): abusing symbol names are: \
|
|
||||||
$(shell nm -g $(LOCAL_PATH)/../../libs/$ARCH/$(F) | cut -c 12- | egrep '.{128}' ) ) \
|
|
||||||
$(info Library $(F) contains symbol names longer than 128 bytes, \
|
|
||||||
YOUR CODE WILL DEADLOCK WITHOUT ANY WARNING when you'll access such function - \
|
|
||||||
please make this library static to avoid problems. ) )
|
|
||||||
$(error Detected libraries with too long symbol names. Remove all files under project/libs/$ARCH, make these libs static, and recompile)
|
|
||||||
endif
|
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
|
|
@ -1,55 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Set here your own NDK path if needed
|
|
||||||
# export PATH=$PATH:~/src/endless_space/android-ndk-r4
|
|
||||||
|
|
||||||
IFS='
|
|
||||||
'
|
|
||||||
|
|
||||||
NDK=`which ndk-build`
|
|
||||||
NDK=`dirname $NDK`
|
|
||||||
GCCVER=4.4.0
|
|
||||||
PLATFORMVER=android-8
|
|
||||||
LOCAL_PATH=`dirname $0`
|
|
||||||
LOCAL_PATH=`cd $LOCAL_PATH && pwd`
|
|
||||||
|
|
||||||
# Hacks for broken configure scripts
|
|
||||||
ln -sf libsdl_mixer.so $LOCAL_PATH/../../bin/ndk/local/armeabi/libSDL_mixer.so
|
|
||||||
ln -sf libsdl_mixer.so $LOCAL_PATH/../../bin/ndk/local/armeabi/libSDL_Mixer.so
|
|
||||||
ln -sf libsdl_net.so $LOCAL_PATH/../../bin/ndk/local/armeabi/libSDL_net.so
|
|
||||||
|
|
||||||
CFLAGS="-I$NDK/build/platforms/$PLATFORMVER/arch-arm/usr/include \
|
|
||||||
-fpic -mthumb-interwork -ffunction-sections -funwind-tables -fstack-protector -fno-short-enums \
|
|
||||||
-D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ -DANDROID \
|
|
||||||
-Wno-psabi -march=armv5te -mtune=xscale -msoft-float -fno-exceptions -fno-rtti -mthumb -Os \
|
|
||||||
-fomit-frame-pointer -fno-strict-aliasing -finline-limit=64 \
|
|
||||||
-Wa,--noexecstack -O2 -DNDEBUG -g \
|
|
||||||
`grep '[-]I[$][(]LOCAL_PATH[)]/[.][.]/' $LOCAL_PATH/Android.mk | tr '\n' ' ' | sed 's/[\\]//g' | sed \"s@[\$][(]LOCAL_PATH[)]/@$LOCAL_PATH/@g\" | sed 's/[ ][ ]*/ /g'`"
|
|
||||||
|
|
||||||
LDFLAGS="-nostdlib -Wl,-soname,libapplication.so -Wl,-shared,-Bsymbolic \
|
|
||||||
-Wl,--whole-archive -Wl,--no-whole-archive \
|
|
||||||
$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/lib/gcc/arm-eabi/4.4.0/libgcc.a \
|
|
||||||
`echo $LOCAL_PATH/../../bin/ndk/local/armeabi/*.so | sed "s@$LOCAL_PATH/../../bin/ndk/local/armeabi/libsdl_main.so@@" | sed "s@$LOCAL_PATH/../../bin/ndk/local/armeabi/libapplication.so@@"` \
|
|
||||||
$NDK/build/platforms/$PLATFORMVER/arch-arm/usr/lib/libc.so \
|
|
||||||
$NDK/build/platforms/$PLATFORMVER/arch-arm/usr/lib/libstdc++.so \
|
|
||||||
$NDK/build/platforms/$PLATFORMVER/arch-arm/usr/lib/libm.so \
|
|
||||||
-Wl,--no-undefined -Wl,-z,noexecstack \
|
|
||||||
-L$NDK/build/platforms/$PLATFORMVER/arch-arm/usr/lib \
|
|
||||||
-lGLESv1_CM -ldl -llog -lz \
|
|
||||||
-Wl,-rpath-link=$NDK/build/platforms/$PLATFORMVER/arch-arm/usr/lib \
|
|
||||||
-L$LOCAL_PATH/../../bin/ndk/local/armeabi"
|
|
||||||
|
|
||||||
env PATH=$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin:$LOCAL_PATH:$PATH \
|
|
||||||
CFLAGS="$CFLAGS" \
|
|
||||||
CXXFLAGS="$CFLAGS" \
|
|
||||||
LDFLAGS="$LDFLAGS" \
|
|
||||||
CC="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-gcc" \
|
|
||||||
CXX="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-g++" \
|
|
||||||
RANLIB="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-ranlib" \
|
|
||||||
LD="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-gcc" \
|
|
||||||
AR="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-ar" \
|
|
||||||
CPP="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-cpp $CFLAGS" \
|
|
||||||
NM="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-nm" \
|
|
||||||
AS="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-as" \
|
|
||||||
STRIP="$NDK/build/prebuilt/linux-x86/arm-eabi-$GCCVER/bin/arm-eabi-strip" \
|
|
||||||
./configure --host=arm-eabi "$@"
|
|
|
@ -1,62 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
LOCAL_PATH=`dirname $0`
|
|
||||||
LOCAL_PATH=`cd $LOCAL_PATH && pwd`
|
|
||||||
|
|
||||||
prefix=$LOCAL_PATH/../sdl
|
|
||||||
exec_prefix=$LOCAL_PATH/../../bin/ndk/local/$ARCH
|
|
||||||
exec_prefix_set=no
|
|
||||||
|
|
||||||
#usage="\
|
|
||||||
#Usage: sdl-config [--prefix[=DIR]] [--exec-prefix[=DIR]] [--version] [--cflags] [--libs]"
|
|
||||||
usage="\
|
|
||||||
Usage: sdl-config [--prefix[=DIR]] [--exec-prefix[=DIR]] [--version] [--cflags] [--libs] [--static-libs]"
|
|
||||||
|
|
||||||
if test $# -eq 0; then
|
|
||||||
echo "${usage}" 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while test $# -gt 0; do
|
|
||||||
case "$1" in
|
|
||||||
-*=*) optarg=`echo "$1" | LC_ALL="C" sed 's/[-_a-zA-Z0-9]*=//'` ;;
|
|
||||||
*) optarg= ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
--prefix=*)
|
|
||||||
prefix=$optarg
|
|
||||||
if test $exec_prefix_set = no ; then
|
|
||||||
exec_prefix=$optarg
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
--prefix)
|
|
||||||
echo $prefix
|
|
||||||
;;
|
|
||||||
--exec-prefix=*)
|
|
||||||
exec_prefix=$optarg
|
|
||||||
exec_prefix_set=yes
|
|
||||||
;;
|
|
||||||
--exec-prefix)
|
|
||||||
echo $exec_prefix
|
|
||||||
;;
|
|
||||||
--version)
|
|
||||||
echo 1.2.14
|
|
||||||
;;
|
|
||||||
--cflags)
|
|
||||||
echo -I${prefix}/include -D_GNU_SOURCE=1 -D_REENTRANT
|
|
||||||
;;
|
|
||||||
--libs)
|
|
||||||
echo -L${exec_prefix} -lsdl
|
|
||||||
;;
|
|
||||||
--static-libs)
|
|
||||||
# --libs|--static-libs)
|
|
||||||
echo -L${exec_prefix} -lsdl
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "${usage}" 1>&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
|
@ -1,13 +0,0 @@
|
||||||
|
|
||||||
/* JNI-C++ wrapper stuff */
|
|
||||||
#ifndef _JNI_WRAPPER_STUFF_H_
|
|
||||||
#define _JNI_WRAPPER_STUFF_H_
|
|
||||||
|
|
||||||
#ifndef SDL_JAVA_PACKAGE_PATH
|
|
||||||
#error You have to define SDL_JAVA_PACKAGE_PATH to your package path with dots replaced with underscores, for example "com_example_SanAngeles"
|
|
||||||
#endif
|
|
||||||
#define JAVA_EXPORT_NAME2(name,package) Java_##package##_##name
|
|
||||||
#define JAVA_EXPORT_NAME1(name,package) JAVA_EXPORT_NAME2(name,package)
|
|
||||||
#define JAVA_EXPORT_NAME(name) JAVA_EXPORT_NAME1(name,SDL_JAVA_PACKAGE_PATH)
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,172 +0,0 @@
|
||||||
#define PY_SSIZE_T_CLEAN
|
|
||||||
#include "Python.h"
|
|
||||||
#ifndef Py_PYTHON_H
|
|
||||||
#error Python headers needed to compile C extensions, please install development version of Python.
|
|
||||||
#else
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <jni.h>
|
|
||||||
#include "SDL.h"
|
|
||||||
#include "android/log.h"
|
|
||||||
#include "jniwrapperstuff.h"
|
|
||||||
|
|
||||||
#define LOG(x) __android_log_write(ANDROID_LOG_INFO, "python", (x))
|
|
||||||
|
|
||||||
static PyObject *androidembed_log(PyObject *self, PyObject *args) {
|
|
||||||
char *logstr = NULL;
|
|
||||||
if (!PyArg_ParseTuple(args, "s", &logstr)) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
LOG(logstr);
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyMethodDef AndroidEmbedMethods[] = {
|
|
||||||
{"log", androidembed_log, METH_VARARGS,
|
|
||||||
"Log on android platform"},
|
|
||||||
{NULL, NULL, 0, NULL}
|
|
||||||
};
|
|
||||||
|
|
||||||
PyMODINIT_FUNC initandroidembed(void) {
|
|
||||||
(void) Py_InitModule("androidembed", AndroidEmbedMethods);
|
|
||||||
}
|
|
||||||
|
|
||||||
int file_exists(const char * filename)
|
|
||||||
{
|
|
||||||
FILE *file;
|
|
||||||
if (file = fopen(filename, "r")) {
|
|
||||||
fclose(file);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
|
||||||
|
|
||||||
char *env_argument = NULL;
|
|
||||||
int ret = 0;
|
|
||||||
FILE *fd;
|
|
||||||
|
|
||||||
LOG("Initialize Python for Android");
|
|
||||||
env_argument = getenv("ANDROID_ARGUMENT");
|
|
||||||
setenv("ANDROID_APP_PATH", env_argument, 1);
|
|
||||||
//setenv("PYTHONVERBOSE", "2", 1);
|
|
||||||
Py_SetProgramName(argv[0]);
|
|
||||||
Py_Initialize();
|
|
||||||
PySys_SetArgv(argc, argv);
|
|
||||||
|
|
||||||
/* ensure threads will work.
|
|
||||||
*/
|
|
||||||
PyEval_InitThreads();
|
|
||||||
|
|
||||||
/* our logging module for android
|
|
||||||
*/
|
|
||||||
initandroidembed();
|
|
||||||
|
|
||||||
/* inject our bootstrap code to redirect python stdin/stdout
|
|
||||||
* replace sys.path with our path
|
|
||||||
*/
|
|
||||||
PyRun_SimpleString(
|
|
||||||
"import sys, posix\n" \
|
|
||||||
"private = posix.environ['ANDROID_PRIVATE']\n" \
|
|
||||||
"argument = posix.environ['ANDROID_ARGUMENT']\n" \
|
|
||||||
"sys.path[:] = [ \n" \
|
|
||||||
" private + '/lib/python27.zip', \n" \
|
|
||||||
" private + '/lib/python2.7/', \n" \
|
|
||||||
" private + '/lib/python2.7/lib-dynload/', \n" \
|
|
||||||
" private + '/lib/python2.7/site-packages/', \n" \
|
|
||||||
" argument ]\n" \
|
|
||||||
"import androidembed\n" \
|
|
||||||
"class LogFile(object):\n" \
|
|
||||||
" def __init__(self):\n" \
|
|
||||||
" self.buffer = ''\n" \
|
|
||||||
" def write(self, s):\n" \
|
|
||||||
" s = self.buffer + s\n" \
|
|
||||||
" lines = s.split(\"\\n\")\n" \
|
|
||||||
" for l in lines[:-1]:\n" \
|
|
||||||
" androidembed.log(l)\n" \
|
|
||||||
" self.buffer = lines[-1]\n" \
|
|
||||||
" def flush(self):\n" \
|
|
||||||
" return\n" \
|
|
||||||
"sys.stdout = sys.stderr = LogFile()\n" \
|
|
||||||
"import site; print site.getsitepackages()\n"\
|
|
||||||
"print 'Android path', sys.path\n" \
|
|
||||||
"print 'Android kivy bootstrap done. __name__ is', __name__");
|
|
||||||
|
|
||||||
/* run it !
|
|
||||||
*/
|
|
||||||
LOG("Run user program, change dir and execute main.py");
|
|
||||||
chdir(env_argument);
|
|
||||||
|
|
||||||
/* search the initial main.py
|
|
||||||
*/
|
|
||||||
char *main_py = "main.pyo";
|
|
||||||
if ( file_exists(main_py) == 0 ) {
|
|
||||||
if ( file_exists("main.py") )
|
|
||||||
main_py = "main.py";
|
|
||||||
else
|
|
||||||
main_py = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( main_py == NULL ) {
|
|
||||||
LOG("No main.pyo / main.py found.");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fd = fopen(main_py, "r");
|
|
||||||
if ( fd == NULL ) {
|
|
||||||
LOG("Open the main.py(o) failed");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* run python !
|
|
||||||
*/
|
|
||||||
ret = PyRun_SimpleFile(fd, main_py);
|
|
||||||
|
|
||||||
if (PyErr_Occurred() != NULL) {
|
|
||||||
ret = 1;
|
|
||||||
PyErr_Print(); /* This exits with the right code if SystemExit. */
|
|
||||||
if (Py_FlushLine())
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* close everything
|
|
||||||
*/
|
|
||||||
Py_Finalize();
|
|
||||||
fclose(fd);
|
|
||||||
|
|
||||||
LOG("Python for android ended.");
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL JAVA_EXPORT_NAME(PythonService_nativeStart) ( JNIEnv* env, jobject thiz,
|
|
||||||
jstring j_android_private,
|
|
||||||
jstring j_android_argument,
|
|
||||||
jstring j_python_home,
|
|
||||||
jstring j_python_path,
|
|
||||||
jstring j_arg )
|
|
||||||
{
|
|
||||||
jboolean iscopy;
|
|
||||||
const char *android_private = (*env)->GetStringUTFChars(env, j_android_private, &iscopy);
|
|
||||||
const char *android_argument = (*env)->GetStringUTFChars(env, j_android_argument, &iscopy);
|
|
||||||
const char *python_home = (*env)->GetStringUTFChars(env, j_python_home, &iscopy);
|
|
||||||
const char *python_path = (*env)->GetStringUTFChars(env, j_python_path, &iscopy);
|
|
||||||
const char *arg = (*env)->GetStringUTFChars(env, j_arg, &iscopy);
|
|
||||||
|
|
||||||
setenv("ANDROID_PRIVATE", android_private, 1);
|
|
||||||
setenv("ANDROID_ARGUMENT", android_argument, 1);
|
|
||||||
setenv("PYTHONOPTIMIZE", "2", 1);
|
|
||||||
setenv("PYTHONHOME", python_home, 1);
|
|
||||||
setenv("PYTHONPATH", python_path, 1);
|
|
||||||
setenv("PYTHON_SERVICE_ARGUMENT", arg, 1);
|
|
||||||
|
|
||||||
char *argv[] = { "service" };
|
|
||||||
/* ANDROID_ARGUMENT points to service subdir,
|
|
||||||
* so main() will run main.py from this dir
|
|
||||||
*/
|
|
||||||
main(1, argv);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1 +0,0 @@
|
||||||
sdk.dir=/opt/android-sdk
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
|
@ -1,39 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/icon"
|
|
||||||
android:layout_width="64sp"
|
|
||||||
android:layout_height="64sp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:padding="2sp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textColor="#fff"
|
|
||||||
android:singleLine="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:id="@+id/author"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="fill_parent"
|
|
||||||
>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="vertical"
|
|
||||||
>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Please choose a project:"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="4sp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/projectList"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="fill_parent"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="vertical"
|
|
||||||
>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/emptyText"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="4sp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="appName">Kivy Launcher</string>
|
|
||||||
<string name="iconName">Kivy Launcher</string>
|
|
||||||
|
|
||||||
<string name="private_version">1323531558.3</string>
|
|
||||||
|
|
||||||
|
|
||||||
<string name="urlScheme">kivy</string>
|
|
||||||
</resources>
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.android.vending.billing;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
interface IMarketBillingService {
|
|
||||||
/** Given the arguments in bundle form, returns a bundle for results. */
|
|
||||||
Bundle sendBillingRequest(in Bundle bundle);
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2012 Kamran Zafar
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kamranzafar.jtar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Kamran Zafar
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class Octal {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse an octal string from a header buffer. This is used for the file
|
|
||||||
* permission mode value.
|
|
||||||
*
|
|
||||||
* @param header
|
|
||||||
* The header buffer from which to parse.
|
|
||||||
* @param offset
|
|
||||||
* The offset into the buffer from which to parse.
|
|
||||||
* @param length
|
|
||||||
* The number of header bytes to parse.
|
|
||||||
*
|
|
||||||
* @return The long value of the octal string.
|
|
||||||
*/
|
|
||||||
public static long parseOctal(byte[] header, int offset, int length) {
|
|
||||||
long result = 0;
|
|
||||||
boolean stillPadding = true;
|
|
||||||
|
|
||||||
int end = offset + length;
|
|
||||||
for (int i = offset; i < end; ++i) {
|
|
||||||
if (header[i] == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (header[i] == (byte) ' ' || header[i] == '0') {
|
|
||||||
if (stillPadding)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (header[i] == (byte) ' ')
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
stillPadding = false;
|
|
||||||
|
|
||||||
result = ( result << 3 ) + ( header[i] - '0' );
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse an octal integer from a header buffer.
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
* @param buf
|
|
||||||
* The header buffer from which to parse.
|
|
||||||
* @param offset
|
|
||||||
* The offset into the buffer from which to parse.
|
|
||||||
* @param length
|
|
||||||
* The number of header bytes to parse.
|
|
||||||
*
|
|
||||||
* @return The integer value of the octal bytes.
|
|
||||||
*/
|
|
||||||
public static int getOctalBytes(long value, byte[] buf, int offset, int length) {
|
|
||||||
int idx = length - 1;
|
|
||||||
|
|
||||||
buf[offset + idx] = 0;
|
|
||||||
--idx;
|
|
||||||
buf[offset + idx] = (byte) ' ';
|
|
||||||
--idx;
|
|
||||||
|
|
||||||
if (value == 0) {
|
|
||||||
buf[offset + idx] = (byte) '0';
|
|
||||||
--idx;
|
|
||||||
} else {
|
|
||||||
for (long val = value; idx >= 0 && val > 0; --idx) {
|
|
||||||
buf[offset + idx] = (byte) ( (byte) '0' + (byte) ( val & 7 ) );
|
|
||||||
val = val >> 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; idx >= 0; --idx) {
|
|
||||||
buf[offset + idx] = (byte) ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
return offset + length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the checksum octal integer from a header buffer.
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
* @param buf
|
|
||||||
* The header buffer from which to parse.
|
|
||||||
* @param offset
|
|
||||||
* The offset into the buffer from which to parse.
|
|
||||||
* @param length
|
|
||||||
* The number of header bytes to parse.
|
|
||||||
* @return The integer value of the entry's checksum.
|
|
||||||
*/
|
|
||||||
public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) {
|
|
||||||
getOctalBytes( value, buf, offset, length );
|
|
||||||
buf[offset + length - 1] = (byte) ' ';
|
|
||||||
buf[offset + length - 2] = 0;
|
|
||||||
return offset + length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse an octal long integer from a header buffer.
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
* @param buf
|
|
||||||
* The header buffer from which to parse.
|
|
||||||
* @param offset
|
|
||||||
* The offset into the buffer from which to parse.
|
|
||||||
* @param length
|
|
||||||
* The number of header bytes to parse.
|
|
||||||
*
|
|
||||||
* @return The long value of the octal bytes.
|
|
||||||
*/
|
|
||||||
public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) {
|
|
||||||
byte[] temp = new byte[length + 1];
|
|
||||||
getOctalBytes( value, temp, 0, length + 1 );
|
|
||||||
System.arraycopy( temp, 0, buf, offset, length );
|
|
||||||
return offset + length;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2012 Kamran Zafar
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kamranzafar.jtar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Kamran Zafar
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class TarConstants {
|
|
||||||
public static final int EOF_BLOCK = 1024;
|
|
||||||
public static final int DATA_BLOCK = 512;
|
|
||||||
public static final int HEADER_BLOCK = 512;
|
|
||||||
}
|
|
|
@ -1,284 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2012 Kamran Zafar
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kamranzafar.jtar;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Kamran Zafar
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class TarEntry {
|
|
||||||
protected File file;
|
|
||||||
protected TarHeader header;
|
|
||||||
|
|
||||||
private TarEntry() {
|
|
||||||
this.file = null;
|
|
||||||
header = new TarHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TarEntry(File file, String entryName) {
|
|
||||||
this();
|
|
||||||
this.file = file;
|
|
||||||
this.extractTarHeader(entryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TarEntry(byte[] headerBuf) {
|
|
||||||
this();
|
|
||||||
this.parseTarHeader(headerBuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor to create an entry from an existing TarHeader object.
|
|
||||||
*
|
|
||||||
* This method is useful to add new entries programmatically (e.g. for
|
|
||||||
* adding files or directories that do not exist in the file system).
|
|
||||||
*
|
|
||||||
* @param header
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public TarEntry(TarHeader header) {
|
|
||||||
this.file = null;
|
|
||||||
this.header = header;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean equals(TarEntry it) {
|
|
||||||
return header.name.toString().equals(it.header.name.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDescendent(TarEntry desc) {
|
|
||||||
return desc.header.name.toString().startsWith(header.name.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public TarHeader getHeader() {
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
String name = header.name.toString();
|
|
||||||
if (header.namePrefix != null && !header.namePrefix.toString().equals("")) {
|
|
||||||
name = header.namePrefix.toString() + "/" + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
header.name = new StringBuffer(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getUserId() {
|
|
||||||
return header.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserId(int userId) {
|
|
||||||
header.userId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getGroupId() {
|
|
||||||
return header.groupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGroupId(int groupId) {
|
|
||||||
header.groupId = groupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUserName() {
|
|
||||||
return header.userName.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserName(String userName) {
|
|
||||||
header.userName = new StringBuffer(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGroupName() {
|
|
||||||
return header.groupName.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGroupName(String groupName) {
|
|
||||||
header.groupName = new StringBuffer(groupName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIds(int userId, int groupId) {
|
|
||||||
this.setUserId(userId);
|
|
||||||
this.setGroupId(groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setModTime(long time) {
|
|
||||||
header.modTime = time / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setModTime(Date time) {
|
|
||||||
header.modTime = time.getTime() / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getModTime() {
|
|
||||||
return new Date(header.modTime * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getFile() {
|
|
||||||
return this.file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getSize() {
|
|
||||||
return header.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSize(long size) {
|
|
||||||
header.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the org.kamrazafar.jtar entry is a directory
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean isDirectory() {
|
|
||||||
if (this.file != null)
|
|
||||||
return this.file.isDirectory();
|
|
||||||
|
|
||||||
if (header != null) {
|
|
||||||
if (header.linkFlag == TarHeader.LF_DIR)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (header.name.toString().endsWith("/"))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract header from File
|
|
||||||
*
|
|
||||||
* @param entryName
|
|
||||||
*/
|
|
||||||
public void extractTarHeader(String entryName) {
|
|
||||||
header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate checksum
|
|
||||||
*
|
|
||||||
* @param buf
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public long computeCheckSum(byte[] buf) {
|
|
||||||
long sum = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < buf.length; ++i) {
|
|
||||||
sum += 255 & buf[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the header to the byte buffer
|
|
||||||
*
|
|
||||||
* @param outbuf
|
|
||||||
*/
|
|
||||||
public void writeEntryHeader(byte[] outbuf) {
|
|
||||||
int offset = 0;
|
|
||||||
|
|
||||||
offset = TarHeader.getNameBytes(header.name, outbuf, offset, TarHeader.NAMELEN);
|
|
||||||
offset = Octal.getOctalBytes(header.mode, outbuf, offset, TarHeader.MODELEN);
|
|
||||||
offset = Octal.getOctalBytes(header.userId, outbuf, offset, TarHeader.UIDLEN);
|
|
||||||
offset = Octal.getOctalBytes(header.groupId, outbuf, offset, TarHeader.GIDLEN);
|
|
||||||
|
|
||||||
long size = header.size;
|
|
||||||
|
|
||||||
offset = Octal.getLongOctalBytes(size, outbuf, offset, TarHeader.SIZELEN);
|
|
||||||
offset = Octal.getLongOctalBytes(header.modTime, outbuf, offset, TarHeader.MODTIMELEN);
|
|
||||||
|
|
||||||
int csOffset = offset;
|
|
||||||
for (int c = 0; c < TarHeader.CHKSUMLEN; ++c)
|
|
||||||
outbuf[offset++] = (byte) ' ';
|
|
||||||
|
|
||||||
outbuf[offset++] = header.linkFlag;
|
|
||||||
|
|
||||||
offset = TarHeader.getNameBytes(header.linkName, outbuf, offset, TarHeader.NAMELEN);
|
|
||||||
offset = TarHeader.getNameBytes(header.magic, outbuf, offset, TarHeader.USTAR_MAGICLEN);
|
|
||||||
offset = TarHeader.getNameBytes(header.userName, outbuf, offset, TarHeader.USTAR_USER_NAMELEN);
|
|
||||||
offset = TarHeader.getNameBytes(header.groupName, outbuf, offset, TarHeader.USTAR_GROUP_NAMELEN);
|
|
||||||
offset = Octal.getOctalBytes(header.devMajor, outbuf, offset, TarHeader.USTAR_DEVLEN);
|
|
||||||
offset = Octal.getOctalBytes(header.devMinor, outbuf, offset, TarHeader.USTAR_DEVLEN);
|
|
||||||
offset = TarHeader.getNameBytes(header.namePrefix, outbuf, offset, TarHeader.USTAR_FILENAME_PREFIX);
|
|
||||||
|
|
||||||
for (; offset < outbuf.length;)
|
|
||||||
outbuf[offset++] = 0;
|
|
||||||
|
|
||||||
long checkSum = this.computeCheckSum(outbuf);
|
|
||||||
|
|
||||||
Octal.getCheckSumOctalBytes(checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the tar header to the byte buffer
|
|
||||||
*
|
|
||||||
* @param header
|
|
||||||
* @param bh
|
|
||||||
*/
|
|
||||||
public void parseTarHeader(byte[] bh) {
|
|
||||||
int offset = 0;
|
|
||||||
|
|
||||||
header.name = TarHeader.parseName(bh, offset, TarHeader.NAMELEN);
|
|
||||||
offset += TarHeader.NAMELEN;
|
|
||||||
|
|
||||||
header.mode = (int) Octal.parseOctal(bh, offset, TarHeader.MODELEN);
|
|
||||||
offset += TarHeader.MODELEN;
|
|
||||||
|
|
||||||
header.userId = (int) Octal.parseOctal(bh, offset, TarHeader.UIDLEN);
|
|
||||||
offset += TarHeader.UIDLEN;
|
|
||||||
|
|
||||||
header.groupId = (int) Octal.parseOctal(bh, offset, TarHeader.GIDLEN);
|
|
||||||
offset += TarHeader.GIDLEN;
|
|
||||||
|
|
||||||
header.size = Octal.parseOctal(bh, offset, TarHeader.SIZELEN);
|
|
||||||
offset += TarHeader.SIZELEN;
|
|
||||||
|
|
||||||
header.modTime = Octal.parseOctal(bh, offset, TarHeader.MODTIMELEN);
|
|
||||||
offset += TarHeader.MODTIMELEN;
|
|
||||||
|
|
||||||
header.checkSum = (int) Octal.parseOctal(bh, offset, TarHeader.CHKSUMLEN);
|
|
||||||
offset += TarHeader.CHKSUMLEN;
|
|
||||||
|
|
||||||
header.linkFlag = bh[offset++];
|
|
||||||
|
|
||||||
header.linkName = TarHeader.parseName(bh, offset, TarHeader.NAMELEN);
|
|
||||||
offset += TarHeader.NAMELEN;
|
|
||||||
|
|
||||||
header.magic = TarHeader.parseName(bh, offset, TarHeader.USTAR_MAGICLEN);
|
|
||||||
offset += TarHeader.USTAR_MAGICLEN;
|
|
||||||
|
|
||||||
header.userName = TarHeader.parseName(bh, offset, TarHeader.USTAR_USER_NAMELEN);
|
|
||||||
offset += TarHeader.USTAR_USER_NAMELEN;
|
|
||||||
|
|
||||||
header.groupName = TarHeader.parseName(bh, offset, TarHeader.USTAR_GROUP_NAMELEN);
|
|
||||||
offset += TarHeader.USTAR_GROUP_NAMELEN;
|
|
||||||
|
|
||||||
header.devMajor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN);
|
|
||||||
offset += TarHeader.USTAR_DEVLEN;
|
|
||||||
|
|
||||||
header.devMinor = (int) Octal.parseOctal(bh, offset, TarHeader.USTAR_DEVLEN);
|
|
||||||
offset += TarHeader.USTAR_DEVLEN;
|
|
||||||
|
|
||||||
header.namePrefix = TarHeader.parseName(bh, offset, TarHeader.USTAR_FILENAME_PREFIX);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,243 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2012 Kamran Zafar
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kamranzafar.jtar;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Header
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Offset Size Field
|
|
||||||
* 0 100 File name
|
|
||||||
* 100 8 File mode
|
|
||||||
* 108 8 Owner's numeric user ID
|
|
||||||
* 116 8 Group's numeric user ID
|
|
||||||
* 124 12 File size in bytes
|
|
||||||
* 136 12 Last modification time in numeric Unix time format
|
|
||||||
* 148 8 Checksum for header block
|
|
||||||
* 156 1 Link indicator (file type)
|
|
||||||
* 157 100 Name of linked file
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* File Types
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Value Meaning
|
|
||||||
* '0' Normal file
|
|
||||||
* (ASCII NUL) Normal file (now obsolete)
|
|
||||||
* '1' Hard link
|
|
||||||
* '2' Symbolic link
|
|
||||||
* '3' Character special
|
|
||||||
* '4' Block special
|
|
||||||
* '5' Directory
|
|
||||||
* '6' FIFO
|
|
||||||
* '7' Contigous
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Ustar header
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Offset Size Field
|
|
||||||
* 257 6 UStar indicator "ustar"
|
|
||||||
* 263 2 UStar version "00"
|
|
||||||
* 265 32 Owner user name
|
|
||||||
* 297 32 Owner group name
|
|
||||||
* 329 8 Device major number
|
|
||||||
* 337 8 Device minor number
|
|
||||||
* 345 155 Filename prefix
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class TarHeader {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Header
|
|
||||||
*/
|
|
||||||
public static final int NAMELEN = 100;
|
|
||||||
public static final int MODELEN = 8;
|
|
||||||
public static final int UIDLEN = 8;
|
|
||||||
public static final int GIDLEN = 8;
|
|
||||||
public static final int SIZELEN = 12;
|
|
||||||
public static final int MODTIMELEN = 12;
|
|
||||||
public static final int CHKSUMLEN = 8;
|
|
||||||
public static final byte LF_OLDNORM = 0;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* File Types
|
|
||||||
*/
|
|
||||||
public static final byte LF_NORMAL = (byte) '0';
|
|
||||||
public static final byte LF_LINK = (byte) '1';
|
|
||||||
public static final byte LF_SYMLINK = (byte) '2';
|
|
||||||
public static final byte LF_CHR = (byte) '3';
|
|
||||||
public static final byte LF_BLK = (byte) '4';
|
|
||||||
public static final byte LF_DIR = (byte) '5';
|
|
||||||
public static final byte LF_FIFO = (byte) '6';
|
|
||||||
public static final byte LF_CONTIG = (byte) '7';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Ustar header
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static final String USTAR_MAGIC = "ustar"; // POSIX
|
|
||||||
|
|
||||||
public static final int USTAR_MAGICLEN = 8;
|
|
||||||
public static final int USTAR_USER_NAMELEN = 32;
|
|
||||||
public static final int USTAR_GROUP_NAMELEN = 32;
|
|
||||||
public static final int USTAR_DEVLEN = 8;
|
|
||||||
public static final int USTAR_FILENAME_PREFIX = 155;
|
|
||||||
|
|
||||||
// Header values
|
|
||||||
public StringBuffer name;
|
|
||||||
public int mode;
|
|
||||||
public int userId;
|
|
||||||
public int groupId;
|
|
||||||
public long size;
|
|
||||||
public long modTime;
|
|
||||||
public int checkSum;
|
|
||||||
public byte linkFlag;
|
|
||||||
public StringBuffer linkName;
|
|
||||||
public StringBuffer magic; // ustar indicator and version
|
|
||||||
public StringBuffer userName;
|
|
||||||
public StringBuffer groupName;
|
|
||||||
public int devMajor;
|
|
||||||
public int devMinor;
|
|
||||||
public StringBuffer namePrefix;
|
|
||||||
|
|
||||||
public TarHeader() {
|
|
||||||
this.magic = new StringBuffer(TarHeader.USTAR_MAGIC);
|
|
||||||
|
|
||||||
this.name = new StringBuffer();
|
|
||||||
this.linkName = new StringBuffer();
|
|
||||||
|
|
||||||
String user = System.getProperty("user.name", "");
|
|
||||||
|
|
||||||
if (user.length() > 31)
|
|
||||||
user = user.substring(0, 31);
|
|
||||||
|
|
||||||
this.userId = 0;
|
|
||||||
this.groupId = 0;
|
|
||||||
this.userName = new StringBuffer(user);
|
|
||||||
this.groupName = new StringBuffer("");
|
|
||||||
this.namePrefix = new StringBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse an entry name from a header buffer.
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* @param header
|
|
||||||
* The header buffer from which to parse.
|
|
||||||
* @param offset
|
|
||||||
* The offset into the buffer from which to parse.
|
|
||||||
* @param length
|
|
||||||
* The number of header bytes to parse.
|
|
||||||
* @return The header's entry name.
|
|
||||||
*/
|
|
||||||
public static StringBuffer parseName(byte[] header, int offset, int length) {
|
|
||||||
StringBuffer result = new StringBuffer(length);
|
|
||||||
|
|
||||||
int end = offset + length;
|
|
||||||
for (int i = offset; i < end; ++i) {
|
|
||||||
if (header[i] == 0)
|
|
||||||
break;
|
|
||||||
result.append((char) header[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the number of bytes in an entry name.
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* @param header
|
|
||||||
* The header buffer from which to parse.
|
|
||||||
* @param offset
|
|
||||||
* The offset into the buffer from which to parse.
|
|
||||||
* @param length
|
|
||||||
* The number of header bytes to parse.
|
|
||||||
* @return The number of bytes in a header's entry name.
|
|
||||||
*/
|
|
||||||
public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) {
|
|
||||||
int i;
|
|
||||||
|
|
||||||
for (i = 0; i < length && i < name.length(); ++i) {
|
|
||||||
buf[offset + i] = (byte) name.charAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; i < length; ++i) {
|
|
||||||
buf[offset + i] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return offset + length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new header for a file/directory entry.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* File name
|
|
||||||
* @param size
|
|
||||||
* File size in bytes
|
|
||||||
* @param modTime
|
|
||||||
* Last modification time in numeric Unix time format
|
|
||||||
* @param dir
|
|
||||||
* Is directory
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static TarHeader createHeader(String entryName, long size, long modTime, boolean dir) {
|
|
||||||
String name = entryName;
|
|
||||||
name = TarUtils.trim(name.replace(File.separatorChar, '/'), '/');
|
|
||||||
|
|
||||||
TarHeader header = new TarHeader();
|
|
||||||
header.linkName = new StringBuffer("");
|
|
||||||
|
|
||||||
if (name.length() > 100) {
|
|
||||||
header.namePrefix = new StringBuffer(name.substring(0, name.lastIndexOf('/')));
|
|
||||||
header.name = new StringBuffer(name.substring(name.lastIndexOf('/') + 1));
|
|
||||||
} else {
|
|
||||||
header.name = new StringBuffer(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dir) {
|
|
||||||
header.mode = 040755;
|
|
||||||
header.linkFlag = TarHeader.LF_DIR;
|
|
||||||
if (header.name.charAt(header.name.length() - 1) != '/') {
|
|
||||||
header.name.append("/");
|
|
||||||
}
|
|
||||||
header.size = 0;
|
|
||||||
} else {
|
|
||||||
header.mode = 0100644;
|
|
||||||
header.linkFlag = TarHeader.LF_NORMAL;
|
|
||||||
header.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
header.modTime = modTime;
|
|
||||||
header.checkSum = 0;
|
|
||||||
header.devMajor = 0;
|
|
||||||
header.devMinor = 0;
|
|
||||||
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2012 Kamran Zafar
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kamranzafar.jtar;
|
|
||||||
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Kamran Zafar
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class TarInputStream extends FilterInputStream {
|
|
||||||
|
|
||||||
private static final int SKIP_BUFFER_SIZE = 2048;
|
|
||||||
private TarEntry currentEntry;
|
|
||||||
private long currentFileSize;
|
|
||||||
private long bytesRead;
|
|
||||||
private boolean defaultSkip = false;
|
|
||||||
|
|
||||||
public TarInputStream(InputStream in) {
|
|
||||||
super(in);
|
|
||||||
currentFileSize = 0;
|
|
||||||
bytesRead = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean markSupported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not supported
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized void mark(int readlimit) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not supported
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public synchronized void reset() throws IOException {
|
|
||||||
throw new IOException("mark/reset not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a byte
|
|
||||||
*
|
|
||||||
* @see java.io.FilterInputStream#read()
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
byte[] buf = new byte[1];
|
|
||||||
|
|
||||||
int res = this.read(buf, 0, 1);
|
|
||||||
|
|
||||||
if (res != -1) {
|
|
||||||
return 0xFF & buf[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the bytes being read exceed the entry size and adjusts the byte
|
|
||||||
* array length. Updates the byte counters
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @see java.io.FilterInputStream#read(byte[], int, int)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
if (currentEntry != null) {
|
|
||||||
if (currentFileSize == currentEntry.getSize()) {
|
|
||||||
return -1;
|
|
||||||
} else if ((currentEntry.getSize() - currentFileSize) < len) {
|
|
||||||
len = (int) (currentEntry.getSize() - currentFileSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int br = super.read(b, off, len);
|
|
||||||
|
|
||||||
if (br != -1) {
|
|
||||||
if (currentEntry != null) {
|
|
||||||
currentFileSize += br;
|
|
||||||
}
|
|
||||||
|
|
||||||
bytesRead += br;
|
|
||||||
}
|
|
||||||
|
|
||||||
return br;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next entry in the tar file
|
|
||||||
*
|
|
||||||
* @return TarEntry
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public TarEntry getNextEntry() throws IOException {
|
|
||||||
closeCurrentEntry();
|
|
||||||
|
|
||||||
byte[] header = new byte[TarConstants.HEADER_BLOCK];
|
|
||||||
byte[] theader = new byte[TarConstants.HEADER_BLOCK];
|
|
||||||
int tr = 0;
|
|
||||||
|
|
||||||
// Read full header
|
|
||||||
while (tr < TarConstants.HEADER_BLOCK) {
|
|
||||||
int res = read(theader, 0, TarConstants.HEADER_BLOCK - tr);
|
|
||||||
|
|
||||||
if (res < 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
System.arraycopy(theader, 0, header, tr, res);
|
|
||||||
tr += res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if record is null
|
|
||||||
boolean eof = true;
|
|
||||||
for (byte b : header) {
|
|
||||||
if (b != 0) {
|
|
||||||
eof = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eof) {
|
|
||||||
currentEntry = new TarEntry(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current offset (in bytes) from the beginning of the stream.
|
|
||||||
* This can be used to find out at which point in a tar file an entry's content begins, for instance.
|
|
||||||
*/
|
|
||||||
public long getCurrentOffset() {
|
|
||||||
return bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the current tar entry
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
protected void closeCurrentEntry() throws IOException {
|
|
||||||
if (currentEntry != null) {
|
|
||||||
if (currentEntry.getSize() > currentFileSize) {
|
|
||||||
// Not fully read, skip rest of the bytes
|
|
||||||
long bs = 0;
|
|
||||||
while (bs < currentEntry.getSize() - currentFileSize) {
|
|
||||||
long res = skip(currentEntry.getSize() - currentFileSize - bs);
|
|
||||||
|
|
||||||
if (res == 0 && currentEntry.getSize() - currentFileSize > 0) {
|
|
||||||
// I suspect file corruption
|
|
||||||
throw new IOException("Possible tar file corruption");
|
|
||||||
}
|
|
||||||
|
|
||||||
bs += res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentEntry = null;
|
|
||||||
currentFileSize = 0L;
|
|
||||||
skipPad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skips the pad at the end of each tar entry file content
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
protected void skipPad() throws IOException {
|
|
||||||
if (bytesRead > 0) {
|
|
||||||
int extra = (int) (bytesRead % TarConstants.DATA_BLOCK);
|
|
||||||
|
|
||||||
if (extra > 0) {
|
|
||||||
long bs = 0;
|
|
||||||
while (bs < TarConstants.DATA_BLOCK - extra) {
|
|
||||||
long res = skip(TarConstants.DATA_BLOCK - extra - bs);
|
|
||||||
bs += res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skips 'n' bytes on the InputStream<br>
|
|
||||||
* Overrides default implementation of skip
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public long skip(long n) throws IOException {
|
|
||||||
if (defaultSkip) {
|
|
||||||
// use skip method of parent stream
|
|
||||||
// may not work if skip not implemented by parent
|
|
||||||
long bs = super.skip(n);
|
|
||||||
bytesRead += bs;
|
|
||||||
|
|
||||||
return bs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n <= 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long left = n;
|
|
||||||
byte[] sBuff = new byte[SKIP_BUFFER_SIZE];
|
|
||||||
|
|
||||||
while (left > 0) {
|
|
||||||
int res = read(sBuff, 0, (int) (left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE));
|
|
||||||
if (res < 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
left -= res;
|
|
||||||
}
|
|
||||||
|
|
||||||
return n - left;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDefaultSkip() {
|
|
||||||
return defaultSkip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultSkip(boolean defaultSkip) {
|
|
||||||
this.defaultSkip = defaultSkip;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2012 Kamran Zafar
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kamranzafar.jtar;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Kamran Zafar
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class TarOutputStream extends OutputStream {
|
|
||||||
private final OutputStream out;
|
|
||||||
private long bytesWritten;
|
|
||||||
private long currentFileSize;
|
|
||||||
private TarEntry currentEntry;
|
|
||||||
|
|
||||||
public TarOutputStream(OutputStream out) {
|
|
||||||
this.out = out;
|
|
||||||
bytesWritten = 0;
|
|
||||||
currentFileSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TarOutputStream(final File fout) throws FileNotFoundException {
|
|
||||||
this.out = new BufferedOutputStream(new FileOutputStream(fout));
|
|
||||||
bytesWritten = 0;
|
|
||||||
currentFileSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a file for writing.
|
|
||||||
*/
|
|
||||||
public TarOutputStream(final File fout, final boolean append) throws IOException {
|
|
||||||
@SuppressWarnings("resource")
|
|
||||||
RandomAccessFile raf = new RandomAccessFile(fout, "rw");
|
|
||||||
final long fileSize = fout.length();
|
|
||||||
if (append && fileSize > TarConstants.EOF_BLOCK) {
|
|
||||||
raf.seek(fileSize - TarConstants.EOF_BLOCK);
|
|
||||||
}
|
|
||||||
out = new BufferedOutputStream(new FileOutputStream(raf.getFD()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends the EOF record and closes the stream
|
|
||||||
*
|
|
||||||
* @see java.io.FilterOutputStream#close()
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
closeCurrentEntry();
|
|
||||||
write( new byte[TarConstants.EOF_BLOCK] );
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Writes a byte to the stream and updates byte counters
|
|
||||||
*
|
|
||||||
* @see java.io.FilterOutputStream#write(int)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void write(int b) throws IOException {
|
|
||||||
out.write( b );
|
|
||||||
bytesWritten += 1;
|
|
||||||
|
|
||||||
if (currentEntry != null) {
|
|
||||||
currentFileSize += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the bytes being written exceed the current entry size.
|
|
||||||
*
|
|
||||||
* @see java.io.FilterOutputStream#write(byte[], int, int)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) throws IOException {
|
|
||||||
if (currentEntry != null && !currentEntry.isDirectory()) {
|
|
||||||
if (currentEntry.getSize() < currentFileSize + len) {
|
|
||||||
throw new IOException( "The current entry[" + currentEntry.getName() + "] size["
|
|
||||||
+ currentEntry.getSize() + "] is smaller than the bytes[" + ( currentFileSize + len )
|
|
||||||
+ "] being written." );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out.write( b, off, len );
|
|
||||||
|
|
||||||
bytesWritten += len;
|
|
||||||
|
|
||||||
if (currentEntry != null) {
|
|
||||||
currentFileSize += len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the next tar entry header on the stream
|
|
||||||
*
|
|
||||||
* @param entry
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public void putNextEntry(TarEntry entry) throws IOException {
|
|
||||||
closeCurrentEntry();
|
|
||||||
|
|
||||||
byte[] header = new byte[TarConstants.HEADER_BLOCK];
|
|
||||||
entry.writeEntryHeader( header );
|
|
||||||
|
|
||||||
write( header );
|
|
||||||
|
|
||||||
currentEntry = entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the current tar entry
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
protected void closeCurrentEntry() throws IOException {
|
|
||||||
if (currentEntry != null) {
|
|
||||||
if (currentEntry.getSize() > currentFileSize) {
|
|
||||||
throw new IOException( "The current entry[" + currentEntry.getName() + "] of size["
|
|
||||||
+ currentEntry.getSize() + "] has not been fully written." );
|
|
||||||
}
|
|
||||||
|
|
||||||
currentEntry = null;
|
|
||||||
currentFileSize = 0;
|
|
||||||
|
|
||||||
pad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pads the last content block
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
protected void pad() throws IOException {
|
|
||||||
if (bytesWritten > 0) {
|
|
||||||
int extra = (int) ( bytesWritten % TarConstants.DATA_BLOCK );
|
|
||||||
|
|
||||||
if (extra > 0) {
|
|
||||||
write( new byte[TarConstants.DATA_BLOCK - extra] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2012 Kamran Zafar
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kamranzafar.jtar;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Kamran
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class TarUtils {
|
|
||||||
/**
|
|
||||||
* Determines the tar file size of the given folder/file path
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static long calculateTarSize(File path) {
|
|
||||||
return tarSize(path) + TarConstants.EOF_BLOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long tarSize(File dir) {
|
|
||||||
long size = 0;
|
|
||||||
|
|
||||||
if (dir.isFile()) {
|
|
||||||
return entrySize(dir.length());
|
|
||||||
} else {
|
|
||||||
File[] subFiles = dir.listFiles();
|
|
||||||
|
|
||||||
if (subFiles != null && subFiles.length > 0) {
|
|
||||||
for (File file : subFiles) {
|
|
||||||
if (file.isFile()) {
|
|
||||||
size += entrySize(file.length());
|
|
||||||
} else {
|
|
||||||
size += tarSize(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Empty folder header
|
|
||||||
return TarConstants.HEADER_BLOCK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long entrySize(long fileSize) {
|
|
||||||
long size = 0;
|
|
||||||
size += TarConstants.HEADER_BLOCK; // Header
|
|
||||||
size += fileSize; // File size
|
|
||||||
|
|
||||||
long extra = size % TarConstants.DATA_BLOCK;
|
|
||||||
|
|
||||||
if (extra > 0) {
|
|
||||||
size += (TarConstants.DATA_BLOCK - extra); // pad
|
|
||||||
}
|
|
||||||
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String trim(String s, char c) {
|
|
||||||
StringBuffer tmp = new StringBuffer(s);
|
|
||||||
for (int i = 0; i < tmp.length(); i++) {
|
|
||||||
if (tmp.charAt(i) != c) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
tmp.deleteCharAt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = tmp.length() - 1; i >= 0; i--) {
|
|
||||||
if (tmp.charAt(i) != c) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
tmp.deleteCharAt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmp.toString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class Action {
|
|
||||||
|
|
||||||
static Context context;
|
|
||||||
|
|
||||||
/* Deliver some data to someone else
|
|
||||||
*/
|
|
||||||
static void send(String mimeType, String filename, String subject, String text, String chooser_title) {
|
|
||||||
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
|
|
||||||
emailIntent.setType(mimeType);
|
|
||||||
/** tryied with String [] emails, but hard to code the whole C/Cython part.
|
|
||||||
if (emails != null)
|
|
||||||
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, emails);
|
|
||||||
**/
|
|
||||||
if (subject != null)
|
|
||||||
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
|
|
||||||
if (text != null)
|
|
||||||
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, text);
|
|
||||||
if (filename != null)
|
|
||||||
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://"+ filename));
|
|
||||||
if (chooser_title == null)
|
|
||||||
chooser_title = "Send mail";
|
|
||||||
context.startActivity(Intent.createChooser(emailIntent, chooser_title));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
// This string is autogenerated by ChangeAppSettings.sh, do not change
|
|
||||||
// spaces amount
|
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import java.util.zip.GZIPInputStream;
|
|
||||||
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
|
|
||||||
import org.kamranzafar.jtar.*;
|
|
||||||
|
|
||||||
class AssetExtract {
|
|
||||||
|
|
||||||
private AssetManager mAssetManager = null;
|
|
||||||
private Activity mActivity = null;
|
|
||||||
|
|
||||||
AssetExtract(Activity act) {
|
|
||||||
mActivity = act;
|
|
||||||
mAssetManager = act.getAssets();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean extractTar(String asset, String target) {
|
|
||||||
|
|
||||||
byte buf[] = new byte[1024 * 1024];
|
|
||||||
|
|
||||||
InputStream assetStream = null;
|
|
||||||
TarInputStream tis = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
|
|
||||||
tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e("python", "opening up extract tar", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
TarEntry entry = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
entry = tis.getNextEntry();
|
|
||||||
} catch ( java.io.IOException e ) {
|
|
||||||
Log.e("python", "extracting tar", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( entry == null ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.v("python", "extracting " + entry.getName());
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
new File(target +"/" + entry.getName()).mkdirs();
|
|
||||||
} catch ( SecurityException e ) { };
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputStream out = null;
|
|
||||||
String path = target + "/" + entry.getName();
|
|
||||||
|
|
||||||
try {
|
|
||||||
out = new BufferedOutputStream(new FileOutputStream(path), 8192);
|
|
||||||
} catch ( FileNotFoundException e ) {
|
|
||||||
} catch ( SecurityException e ) { };
|
|
||||||
|
|
||||||
if ( out == null ) {
|
|
||||||
Log.e("python", "could not open " + path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
int len = tis.read(buf);
|
|
||||||
|
|
||||||
if (len == -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
out.write(buf, 0, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
out.flush();
|
|
||||||
out.close();
|
|
||||||
} catch ( java.io.IOException e ) {
|
|
||||||
Log.e("python", "extracting zip", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
tis.close();
|
|
||||||
assetStream.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
/*
|
|
||||||
Simple DirectMedia Layer
|
|
||||||
Java source code (C) 2009-2011 Sergii Pylypenko
|
|
||||||
|
|
||||||
This software is provided 'as-is', without any express or implied
|
|
||||||
warranty. In no event will the authors be held liable for any damages
|
|
||||||
arising from the use of this software.
|
|
||||||
|
|
||||||
Permission is granted to anyone to use this software for any purpose,
|
|
||||||
including commercial applications, and to alter it and redistribute it
|
|
||||||
freely, subject to the following restrictions:
|
|
||||||
|
|
||||||
1. The origin of this software must not be misrepresented; you must not
|
|
||||||
claim that you wrote the original software. If you use this software
|
|
||||||
in a product, an acknowledgment in the product documentation would be
|
|
||||||
appreciated but is not required.
|
|
||||||
2. Altered source versions must be plainly marked as such, and must not be
|
|
||||||
misrepresented as being the original software.
|
|
||||||
3. This notice may not be removed or altered from any source distribution.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.media.AudioTrack;
|
|
||||||
import android.media.AudioManager;
|
|
||||||
import android.media.AudioFormat;
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import android.util.Log;
|
|
||||||
import java.lang.Thread;
|
|
||||||
|
|
||||||
|
|
||||||
class AudioThread {
|
|
||||||
|
|
||||||
private PythonActivity mParent;
|
|
||||||
private AudioTrack mAudio;
|
|
||||||
private byte[] mAudioBuffer;
|
|
||||||
private int mVirtualBufSize;
|
|
||||||
|
|
||||||
public AudioThread(PythonActivity parent)
|
|
||||||
{
|
|
||||||
mParent = parent;
|
|
||||||
mAudio = null;
|
|
||||||
mAudioBuffer = null;
|
|
||||||
nativeAudioInitJavaCallbacks();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int fillBuffer()
|
|
||||||
{
|
|
||||||
if( mParent.isPaused() )
|
|
||||||
{
|
|
||||||
try{
|
|
||||||
Thread.sleep(200);
|
|
||||||
} catch (InterruptedException e) {}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//if( Globals.AudioBufferConfig == 0 ) // Gives too much spam to logcat, makes things worse
|
|
||||||
// mAudio.flush();
|
|
||||||
|
|
||||||
mAudio.write( mAudioBuffer, 0, mVirtualBufSize );
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int initAudio(int rate, int channels, int encoding, int bufSize)
|
|
||||||
{
|
|
||||||
if( mAudio == null )
|
|
||||||
{
|
|
||||||
channels = ( channels == 1 ) ? AudioFormat.CHANNEL_CONFIGURATION_MONO :
|
|
||||||
AudioFormat.CHANNEL_CONFIGURATION_STEREO;
|
|
||||||
encoding = ( encoding == 1 ) ? AudioFormat.ENCODING_PCM_16BIT :
|
|
||||||
AudioFormat.ENCODING_PCM_8BIT;
|
|
||||||
|
|
||||||
mVirtualBufSize = bufSize;
|
|
||||||
|
|
||||||
if( AudioTrack.getMinBufferSize( rate, channels, encoding ) > bufSize )
|
|
||||||
bufSize = AudioTrack.getMinBufferSize( rate, channels, encoding );
|
|
||||||
|
|
||||||
/**
|
|
||||||
if(Globals.AudioBufferConfig != 0) { // application's choice - use minimal buffer
|
|
||||||
bufSize = (int)((float)bufSize * (((float)(Globals.AudioBufferConfig - 1) * 2.5f) + 1.0f));
|
|
||||||
mVirtualBufSize = bufSize;
|
|
||||||
}
|
|
||||||
**/
|
|
||||||
mAudioBuffer = new byte[bufSize];
|
|
||||||
|
|
||||||
mAudio = new AudioTrack(AudioManager.STREAM_MUSIC,
|
|
||||||
rate,
|
|
||||||
channels,
|
|
||||||
encoding,
|
|
||||||
bufSize,
|
|
||||||
AudioTrack.MODE_STREAM );
|
|
||||||
mAudio.play();
|
|
||||||
}
|
|
||||||
return mVirtualBufSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getBuffer()
|
|
||||||
{
|
|
||||||
return mAudioBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int deinitAudio()
|
|
||||||
{
|
|
||||||
if( mAudio != null )
|
|
||||||
{
|
|
||||||
mAudio.stop();
|
|
||||||
mAudio.release();
|
|
||||||
mAudio = null;
|
|
||||||
}
|
|
||||||
mAudioBuffer = null;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int initAudioThread()
|
|
||||||
{
|
|
||||||
// Make audio thread priority higher so audio thread won't get underrun
|
|
||||||
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int pauseAudioPlayback()
|
|
||||||
{
|
|
||||||
if( mAudio != null )
|
|
||||||
{
|
|
||||||
mAudio.pause();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int resumeAudioPlayback()
|
|
||||||
{
|
|
||||||
if( mAudio != null )
|
|
||||||
{
|
|
||||||
mAudio.play();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private native int nativeAudioInitJavaCallbacks();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class GenericBroadcastReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
GenericBroadcastReceiverCallback listener;
|
|
||||||
|
|
||||||
public GenericBroadcastReceiver(GenericBroadcastReceiverCallback listener) {
|
|
||||||
super();
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
this.listener.onReceive(context, intent);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public interface GenericBroadcastReceiverCallback {
|
|
||||||
void onReceive(Context context, Intent intent);
|
|
||||||
};
|
|
|
@ -1,284 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Vibrator;
|
|
||||||
import android.hardware.Sensor;
|
|
||||||
import android.hardware.SensorEvent;
|
|
||||||
import android.hardware.SensorEventListener;
|
|
||||||
import android.hardware.SensorManager;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import android.net.wifi.ScanResult;
|
|
||||||
import android.net.wifi.WifiManager;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.NetworkInfo;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Methods that are expected to be called via JNI, to access the
|
|
||||||
* device's non-screen hardware. (For example, the vibration and
|
|
||||||
* accelerometer.)
|
|
||||||
*/
|
|
||||||
public class Hardware {
|
|
||||||
|
|
||||||
// The context.
|
|
||||||
static Context context;
|
|
||||||
static View view;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vibrate for s seconds.
|
|
||||||
*/
|
|
||||||
public static void vibrate(double s) {
|
|
||||||
Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
|
||||||
if (v != null) {
|
|
||||||
v.vibrate((int) (1000 * s));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an Overview of all Hardware Sensors of an Android Device
|
|
||||||
*/
|
|
||||||
public static String getHardwareSensors() {
|
|
||||||
SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
|
||||||
List<Sensor> allSensors = sm.getSensorList(Sensor.TYPE_ALL);
|
|
||||||
|
|
||||||
if (allSensors != null) {
|
|
||||||
String resultString = "";
|
|
||||||
for (Sensor s : allSensors) {
|
|
||||||
resultString += String.format("Name=" + s.getName());
|
|
||||||
resultString += String.format(",Vendor=" + s.getVendor());
|
|
||||||
resultString += String.format(",Version=" + s.getVersion());
|
|
||||||
resultString += String.format(",MaximumRange=" + s.getMaximumRange());
|
|
||||||
// XXX MinDelay is not in the 2.2
|
|
||||||
//resultString += String.format(",MinDelay=" + s.getMinDelay());
|
|
||||||
resultString += String.format(",Power=" + s.getPower());
|
|
||||||
resultString += String.format(",Type=" + s.getType() + "\n");
|
|
||||||
}
|
|
||||||
return resultString;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Access to 3 Axis Hardware Sensors Accelerometer, Orientation and Magnetic Field Sensors
|
|
||||||
*/
|
|
||||||
public static class generic3AxisSensor implements SensorEventListener {
|
|
||||||
private final SensorManager sSensorManager;
|
|
||||||
private final Sensor sSensor;
|
|
||||||
private final int sSensorType;
|
|
||||||
SensorEvent sSensorEvent;
|
|
||||||
|
|
||||||
public generic3AxisSensor(int sensorType) {
|
|
||||||
sSensorType = sensorType;
|
|
||||||
sSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
|
||||||
sSensor = sSensorManager.getDefaultSensor(sSensorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onSensorChanged(SensorEvent event) {
|
|
||||||
sSensorEvent = event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable the Sensor by registering/unregistering
|
|
||||||
*/
|
|
||||||
public void changeStatus(boolean enable) {
|
|
||||||
if (enable) {
|
|
||||||
sSensorManager.registerListener(this, sSensor, SensorManager.SENSOR_DELAY_NORMAL);
|
|
||||||
} else {
|
|
||||||
sSensorManager.unregisterListener(this, sSensor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the Sensor
|
|
||||||
*/
|
|
||||||
public float[] readSensor() {
|
|
||||||
if (sSensorEvent != null) {
|
|
||||||
return sSensorEvent.values;
|
|
||||||
} else {
|
|
||||||
float rv[] = { 0f, 0f, 0f };
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static generic3AxisSensor accelerometerSensor = null;
|
|
||||||
public static generic3AxisSensor orientationSensor = null;
|
|
||||||
public static generic3AxisSensor magneticFieldSensor = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* functions for backward compatibility reasons
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static void accelerometerEnable(boolean enable) {
|
|
||||||
if ( accelerometerSensor == null )
|
|
||||||
accelerometerSensor = new generic3AxisSensor(Sensor.TYPE_ACCELEROMETER);
|
|
||||||
accelerometerSensor.changeStatus(enable);
|
|
||||||
}
|
|
||||||
public static float[] accelerometerReading() {
|
|
||||||
float rv[] = { 0f, 0f, 0f };
|
|
||||||
if ( accelerometerSensor == null )
|
|
||||||
return rv;
|
|
||||||
return (float[]) accelerometerSensor.readSensor();
|
|
||||||
}
|
|
||||||
public static void orientationSensorEnable(boolean enable) {
|
|
||||||
if ( orientationSensor == null )
|
|
||||||
orientationSensor = new generic3AxisSensor(Sensor.TYPE_ORIENTATION);
|
|
||||||
orientationSensor.changeStatus(enable);
|
|
||||||
}
|
|
||||||
public static float[] orientationSensorReading() {
|
|
||||||
float rv[] = { 0f, 0f, 0f };
|
|
||||||
if ( orientationSensor == null )
|
|
||||||
return rv;
|
|
||||||
return (float[]) orientationSensor.readSensor();
|
|
||||||
}
|
|
||||||
public static void magneticFieldSensorEnable(boolean enable) {
|
|
||||||
if ( magneticFieldSensor == null )
|
|
||||||
magneticFieldSensor = new generic3AxisSensor(Sensor.TYPE_MAGNETIC_FIELD);
|
|
||||||
magneticFieldSensor.changeStatus(enable);
|
|
||||||
}
|
|
||||||
public static float[] magneticFieldSensorReading() {
|
|
||||||
float rv[] = { 0f, 0f, 0f };
|
|
||||||
if ( magneticFieldSensor == null )
|
|
||||||
return rv;
|
|
||||||
return (float[]) magneticFieldSensor.readSensor();
|
|
||||||
}
|
|
||||||
|
|
||||||
static public DisplayMetrics metrics = new DisplayMetrics();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get display DPI.
|
|
||||||
*/
|
|
||||||
public static int getDPI() {
|
|
||||||
return metrics.densityDpi;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the soft keyboard.
|
|
||||||
*/
|
|
||||||
public static void showKeyboard(int input_type) {
|
|
||||||
//Log.i("python", "hardware.Java show_keyword " input_type);
|
|
||||||
|
|
||||||
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
|
|
||||||
SDLSurfaceView vw = (SDLSurfaceView) view;
|
|
||||||
|
|
||||||
int inputType = input_type;
|
|
||||||
|
|
||||||
if (vw.inputType != inputType){
|
|
||||||
vw.inputType = inputType;
|
|
||||||
imm.restartInput(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
imm.showSoftInput(view, InputMethodManager.SHOW_FORCED);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the soft keyboard.
|
|
||||||
*/
|
|
||||||
public static void hideKeyboard() {
|
|
||||||
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan WiFi networks
|
|
||||||
*/
|
|
||||||
static List<ScanResult> latestResult;
|
|
||||||
|
|
||||||
public static void enableWifiScanner()
|
|
||||||
{
|
|
||||||
IntentFilter i = new IntentFilter();
|
|
||||||
i.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
|
|
||||||
|
|
||||||
context.registerReceiver(new BroadcastReceiver() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context c, Intent i) {
|
|
||||||
// Code to execute when SCAN_RESULTS_AVAILABLE_ACTION event occurs
|
|
||||||
WifiManager w = (WifiManager) c.getSystemService(Context.WIFI_SERVICE);
|
|
||||||
latestResult = w.getScanResults(); // Returns a <list> of scanResults
|
|
||||||
}
|
|
||||||
|
|
||||||
}, i);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String scanWifi() {
|
|
||||||
|
|
||||||
// Now you can call this and it should execute the broadcastReceiver's
|
|
||||||
// onReceive()
|
|
||||||
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
|
||||||
boolean a = wm.startScan();
|
|
||||||
|
|
||||||
if (latestResult != null){
|
|
||||||
|
|
||||||
String latestResultString = "";
|
|
||||||
for (ScanResult result : latestResult)
|
|
||||||
{
|
|
||||||
latestResultString += String.format("%s\t%s\t%d\n", result.SSID, result.BSSID, result.level);
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestResultString;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* network state
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static boolean network_state = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check network state directly
|
|
||||||
*
|
|
||||||
* (only one connection can be active at a given moment, detects all network type)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public static boolean checkNetwork()
|
|
||||||
{
|
|
||||||
boolean state = false;
|
|
||||||
final ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
|
|
||||||
final NetworkInfo activeNetwork = conMgr.getActiveNetworkInfo();
|
|
||||||
if (activeNetwork != null && activeNetwork.isConnected()) {
|
|
||||||
state = true;
|
|
||||||
} else {
|
|
||||||
state = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To recieve network state changes
|
|
||||||
*/
|
|
||||||
public static void registerNetworkCheck()
|
|
||||||
{
|
|
||||||
IntentFilter i = new IntentFilter();
|
|
||||||
i.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
|
||||||
context.registerReceiver(new BroadcastReceiver() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context c, Intent i) {
|
|
||||||
network_state = checkNetwork();
|
|
||||||
}
|
|
||||||
|
|
||||||
}, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This represents a project we've scanned for.
|
|
||||||
*/
|
|
||||||
public class Project {
|
|
||||||
|
|
||||||
String dir = null;
|
|
||||||
String title = null;
|
|
||||||
String author = null;
|
|
||||||
Bitmap icon = null;
|
|
||||||
boolean landscape = false;
|
|
||||||
|
|
||||||
static String decode(String s) {
|
|
||||||
try {
|
|
||||||
return new String(s.getBytes("ISO-8859-1"), "UTF-8");
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scans directory for a project.txt file. If it finds one,
|
|
||||||
* and it looks valid enough, then it creates a new Project,
|
|
||||||
* and returns that. Otherwise, returns null.
|
|
||||||
*/
|
|
||||||
public static Project scanDirectory(File dir) {
|
|
||||||
|
|
||||||
// We might have a link file.
|
|
||||||
if (dir.getAbsolutePath().endsWith(".link")) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Scan the android.txt file.
|
|
||||||
File propfile = new File(dir, "android.txt");
|
|
||||||
FileInputStream in = new FileInputStream(propfile);
|
|
||||||
Properties p = new Properties();
|
|
||||||
p.load(in);
|
|
||||||
in.close();
|
|
||||||
|
|
||||||
String directory = p.getProperty("directory", null);
|
|
||||||
|
|
||||||
if (directory == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
dir = new File(directory);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.i("Project", "Couldn't open link file " + dir, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we're dealing with a directory.
|
|
||||||
if (! dir.isDirectory()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Scan the android.txt file.
|
|
||||||
File propfile = new File(dir, "android.txt");
|
|
||||||
FileInputStream in = new FileInputStream(propfile);
|
|
||||||
Properties p = new Properties();
|
|
||||||
p.load(in);
|
|
||||||
in.close();
|
|
||||||
|
|
||||||
// Get the various properties.
|
|
||||||
String title = decode(p.getProperty("title", "Untitled"));
|
|
||||||
String author = decode(p.getProperty("author", ""));
|
|
||||||
boolean landscape = p.getProperty("orientation", "portrait").equals("landscape");
|
|
||||||
|
|
||||||
// Create the project object.
|
|
||||||
Project rv = new Project();
|
|
||||||
rv.title = title;
|
|
||||||
rv.author = author;
|
|
||||||
rv.icon = BitmapFactory.decodeFile(new File(dir, "icon.png").getAbsolutePath());
|
|
||||||
rv.landscape = landscape;
|
|
||||||
rv.dir = dir.getAbsolutePath();
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.i("Project", "Couldn't open android.txt", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class ProjectAdapter extends ArrayAdapter<Project> {
|
|
||||||
|
|
||||||
private Activity mContext;
|
|
||||||
private ResourceManager resourceManager;
|
|
||||||
|
|
||||||
|
|
||||||
public ProjectAdapter(Activity context) {
|
|
||||||
super(context, 0);
|
|
||||||
|
|
||||||
mContext = context;
|
|
||||||
resourceManager = new ResourceManager(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
Project p = getItem(position);
|
|
||||||
|
|
||||||
View v = resourceManager.inflateView("chooser_item");
|
|
||||||
TextView title = (TextView) resourceManager.getViewById(v, "title");
|
|
||||||
TextView author = (TextView) resourceManager.getViewById(v, "author");
|
|
||||||
ImageView icon = (ImageView) resourceManager.getViewById(v, "icon");
|
|
||||||
|
|
||||||
title.setText(p.title);
|
|
||||||
author.setText(p.author);
|
|
||||||
icon.setImageBitmap(p.icon);
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
public class ProjectChooser extends Activity implements AdapterView.OnItemClickListener {
|
|
||||||
|
|
||||||
ResourceManager resourceManager;
|
|
||||||
|
|
||||||
String urlScheme;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart()
|
|
||||||
{
|
|
||||||
super.onStart();
|
|
||||||
|
|
||||||
resourceManager = new ResourceManager(this);
|
|
||||||
|
|
||||||
urlScheme = resourceManager.getString("urlScheme");
|
|
||||||
|
|
||||||
// Set the window title.
|
|
||||||
setTitle(resourceManager.getString("appName"));
|
|
||||||
|
|
||||||
// Scan the sdcard for files, and sort them.
|
|
||||||
File dir = new File(Environment.getExternalStorageDirectory(), urlScheme);
|
|
||||||
|
|
||||||
File entries[] = dir.listFiles();
|
|
||||||
|
|
||||||
if (entries == null) {
|
|
||||||
entries = new File[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
Arrays.sort(entries);
|
|
||||||
|
|
||||||
// Create a ProjectAdapter and fill it with projects.
|
|
||||||
ProjectAdapter projectAdapter = new ProjectAdapter(this);
|
|
||||||
|
|
||||||
// Populate it with the properties files.
|
|
||||||
for (File d : entries) {
|
|
||||||
Project p = Project.scanDirectory(d);
|
|
||||||
if (p != null) {
|
|
||||||
projectAdapter.add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectAdapter.getCount() != 0) {
|
|
||||||
|
|
||||||
View v = resourceManager.inflateView("project_chooser");
|
|
||||||
ListView l = (ListView) resourceManager.getViewById(v, "projectList");
|
|
||||||
|
|
||||||
l.setAdapter(projectAdapter);
|
|
||||||
l.setOnItemClickListener(this);
|
|
||||||
|
|
||||||
setContentView(v);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
View v = resourceManager.inflateView("project_empty");
|
|
||||||
TextView emptyText = (TextView) resourceManager.getViewById(v, "emptyText");
|
|
||||||
|
|
||||||
emptyText.setText("No projects are available to launch. Please place a project into " + dir + " and restart this application. Press the back button to exit.");
|
|
||||||
|
|
||||||
setContentView(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onItemClick(AdapterView parent, View view, int position, long id) {
|
|
||||||
Project p = (Project) parent.getItemAtPosition(position);
|
|
||||||
|
|
||||||
Intent intent = new Intent(
|
|
||||||
"org.renpy.LAUNCH",
|
|
||||||
Uri.fromParts(urlScheme, p.dir, ""));
|
|
||||||
|
|
||||||
intent.setClassName(getPackageName(), "org.renpy.android.PythonActivity");
|
|
||||||
this.startActivity(intent);
|
|
||||||
this.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,627 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.ActivityInfo;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
// Billing
|
|
||||||
import org.renpy.android.Configuration;
|
|
||||||
import org.renpy.android.billing.BillingService.RequestPurchase;
|
|
||||||
import org.renpy.android.billing.BillingService.RestoreTransactions;
|
|
||||||
import org.renpy.android.billing.Consts.PurchaseState;
|
|
||||||
import org.renpy.android.billing.Consts.ResponseCode;
|
|
||||||
import org.renpy.android.billing.PurchaseObserver;
|
|
||||||
import org.renpy.android.billing.BillingService;
|
|
||||||
import org.renpy.android.billing.PurchaseDatabase;
|
|
||||||
import org.renpy.android.billing.Consts;
|
|
||||||
import org.renpy.android.billing.ResponseHandler;
|
|
||||||
import org.renpy.android.billing.Security;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
|
|
||||||
public class PythonActivity extends Activity implements Runnable {
|
|
||||||
private static String TAG = "Python";
|
|
||||||
|
|
||||||
// The audio thread for streaming audio...
|
|
||||||
private static AudioThread mAudioThread = null;
|
|
||||||
|
|
||||||
// The SDLSurfaceView we contain.
|
|
||||||
public static SDLSurfaceView mView = null;
|
|
||||||
public static PythonActivity mActivity = null;
|
|
||||||
public static ApplicationInfo mInfo = null;
|
|
||||||
|
|
||||||
// Did we launch our thread?
|
|
||||||
private boolean mLaunchedThread = false;
|
|
||||||
|
|
||||||
private ResourceManager resourceManager;
|
|
||||||
|
|
||||||
// The path to the directory contaning our external storage.
|
|
||||||
private File externalStorage;
|
|
||||||
|
|
||||||
// The path to the directory containing the game.
|
|
||||||
private File mPath = null;
|
|
||||||
|
|
||||||
boolean _isPaused = false;
|
|
||||||
|
|
||||||
private static final String DB_INITIALIZED = "db_initialized";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
Hardware.context = this;
|
|
||||||
Action.context = this;
|
|
||||||
this.mActivity = this;
|
|
||||||
|
|
||||||
getWindowManager().getDefaultDisplay().getMetrics(Hardware.metrics);
|
|
||||||
|
|
||||||
resourceManager = new ResourceManager(this);
|
|
||||||
externalStorage = new File(Environment.getExternalStorageDirectory(), getPackageName());
|
|
||||||
|
|
||||||
// Figure out the directory where the game is. If the game was
|
|
||||||
// given to us via an intent, then we use the scheme-specific
|
|
||||||
// part of that intent to determine the file to launch. We
|
|
||||||
// also use the android.txt file to determine the orientation.
|
|
||||||
//
|
|
||||||
// Otherwise, we use the public data, if we have it, or the
|
|
||||||
// private data if we do not.
|
|
||||||
if (getIntent() != null && getIntent().getAction() != null &&
|
|
||||||
getIntent().getAction().equals("org.renpy.LAUNCH")) {
|
|
||||||
mPath = new File(getIntent().getData().getSchemeSpecificPart());
|
|
||||||
|
|
||||||
Project p = Project.scanDirectory(mPath);
|
|
||||||
|
|
||||||
if (p != null) {
|
|
||||||
if (p.landscape) {
|
|
||||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
|
||||||
} else {
|
|
||||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let old apps know they started.
|
|
||||||
try {
|
|
||||||
FileWriter f = new FileWriter(new File(mPath, ".launch"));
|
|
||||||
f.write("started");
|
|
||||||
f.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} else if (resourceManager.getString("public_version") != null) {
|
|
||||||
mPath = externalStorage;
|
|
||||||
} else {
|
|
||||||
mPath = getFilesDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
||||||
|
|
||||||
// go to fullscreen mode if requested
|
|
||||||
try {
|
|
||||||
this.mInfo = this.getPackageManager().getApplicationInfo(
|
|
||||||
this.getPackageName(), PackageManager.GET_META_DATA);
|
|
||||||
Log.v("python", "metadata fullscreen is" + this.mInfo.metaData.get("fullscreen"));
|
|
||||||
if ( (Integer)this.mInfo.metaData.get("fullscreen") == 1 ) {
|
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
}
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( Configuration.use_billing ) {
|
|
||||||
mBillingHandler = new Handler();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start showing an SDLSurfaceView.
|
|
||||||
mView = new SDLSurfaceView(
|
|
||||||
this,
|
|
||||||
mPath.getAbsolutePath());
|
|
||||||
|
|
||||||
Hardware.view = mView;
|
|
||||||
setContentView(mView);
|
|
||||||
|
|
||||||
// Force the background window color if asked
|
|
||||||
if ( this.mInfo.metaData.containsKey("android.background_color") ) {
|
|
||||||
getWindow().getDecorView().setBackgroundColor(
|
|
||||||
this.mInfo.metaData.getInt("android.background_color"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an error using a toast. (Only makes sense from non-UI
|
|
||||||
* threads.)
|
|
||||||
*/
|
|
||||||
public void toastError(final String msg) {
|
|
||||||
|
|
||||||
final Activity thisActivity = this;
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable () {
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait to show the error.
|
|
||||||
synchronized (this) {
|
|
||||||
try {
|
|
||||||
this.wait(1000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recursiveDelete(File f) {
|
|
||||||
if (f.isDirectory()) {
|
|
||||||
for (File r : f.listFiles()) {
|
|
||||||
recursiveDelete(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This determines if unpacking one the zip files included in
|
|
||||||
* the .apk is necessary. If it is, the zip file is unpacked.
|
|
||||||
*/
|
|
||||||
public void unpackData(final String resource, File target) {
|
|
||||||
|
|
||||||
// The version of data in memory and on disk.
|
|
||||||
String data_version = resourceManager.getString(resource + "_version");
|
|
||||||
String disk_version = null;
|
|
||||||
|
|
||||||
// If no version, no unpacking is necessary.
|
|
||||||
if (data_version == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the current disk version, if any.
|
|
||||||
String filesDir = target.getAbsolutePath();
|
|
||||||
String disk_version_fn = filesDir + "/" + resource + ".version";
|
|
||||||
|
|
||||||
try {
|
|
||||||
byte buf[] = new byte[64];
|
|
||||||
InputStream is = new FileInputStream(disk_version_fn);
|
|
||||||
int len = is.read(buf);
|
|
||||||
disk_version = new String(buf, 0, len);
|
|
||||||
is.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
disk_version = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the disk data is out of date, extract it and write the
|
|
||||||
// version file.
|
|
||||||
if (! data_version.equals(disk_version)) {
|
|
||||||
Log.v(TAG, "Extracting " + resource + " assets.");
|
|
||||||
|
|
||||||
recursiveDelete(target);
|
|
||||||
target.mkdirs();
|
|
||||||
|
|
||||||
AssetExtract ae = new AssetExtract(this);
|
|
||||||
if (!ae.extractTar(resource + ".mp3", target.getAbsolutePath())) {
|
|
||||||
toastError("Could not extract " + resource + " data.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Write .nomedia.
|
|
||||||
new File(target, ".nomedia").createNewFile();
|
|
||||||
|
|
||||||
// Write version file.
|
|
||||||
FileOutputStream os = new FileOutputStream(disk_version_fn);
|
|
||||||
os.write(data_version.getBytes());
|
|
||||||
os.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w("python", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
|
|
||||||
unpackData("private", getFilesDir());
|
|
||||||
unpackData("public", externalStorage);
|
|
||||||
|
|
||||||
System.loadLibrary("sdl");
|
|
||||||
System.loadLibrary("sdl_image");
|
|
||||||
System.loadLibrary("sdl_ttf");
|
|
||||||
System.loadLibrary("sdl_mixer");
|
|
||||||
System.loadLibrary("python2.7");
|
|
||||||
System.loadLibrary("application");
|
|
||||||
System.loadLibrary("sdl_main");
|
|
||||||
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_io.so");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/unicodedata.so");
|
|
||||||
|
|
||||||
try {
|
|
||||||
System.loadLibrary("sqlite3");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_sqlite3.so");
|
|
||||||
} catch(UnsatisfiedLinkError e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imaging.so");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imagingft.so");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imagingmath.so");
|
|
||||||
} catch(UnsatisfiedLinkError e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( mAudioThread == null ) {
|
|
||||||
Log.i("python", "Starting audio thread");
|
|
||||||
mAudioThread = new AudioThread(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable () {
|
|
||||||
public void run() {
|
|
||||||
mView.start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
_isPaused = true;
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
if (mView != null) {
|
|
||||||
mView.onPause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
_isPaused = false;
|
|
||||||
|
|
||||||
if (!mLaunchedThread) {
|
|
||||||
mLaunchedThread = true;
|
|
||||||
new Thread(this).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mView != null) {
|
|
||||||
mView.onResume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPaused() {
|
|
||||||
return _isPaused;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyDown(int keyCode, final KeyEvent event) {
|
|
||||||
//Log.i("python", "key2 " + mView + " " + mView.mStarted);
|
|
||||||
if (mView != null && mView.mStarted && SDLSurfaceView.nativeKey(keyCode, 1, event.getUnicodeChar())) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyUp(int keyCode, final KeyEvent event) {
|
|
||||||
//Log.i("python", "key up " + mView + " " + mView.mStarted);
|
|
||||||
if (mView != null && mView.mStarted && SDLSurfaceView.nativeKey(keyCode, 0, event.getUnicodeChar())) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return super.onKeyUp(keyCode, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onDestroy() {
|
|
||||||
mPurchaseDatabase.close();
|
|
||||||
mBillingService.unbind();
|
|
||||||
|
|
||||||
if (mView != null) {
|
|
||||||
mView.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Log.i(TAG, "on destroy (exit1)");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void start_service(String serviceTitle, String serviceDescription,
|
|
||||||
String pythonServiceArgument) {
|
|
||||||
Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
|
|
||||||
String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath();
|
|
||||||
String filesDirectory = PythonActivity.mActivity.mPath.getAbsolutePath();
|
|
||||||
serviceIntent.putExtra("androidPrivate", argument);
|
|
||||||
serviceIntent.putExtra("androidArgument", filesDirectory);
|
|
||||||
serviceIntent.putExtra("pythonHome", argument);
|
|
||||||
serviceIntent.putExtra("pythonPath", argument + ":" + filesDirectory + "/lib");
|
|
||||||
serviceIntent.putExtra("serviceTitle", serviceTitle);
|
|
||||||
serviceIntent.putExtra("serviceDescription", serviceDescription);
|
|
||||||
serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument);
|
|
||||||
PythonActivity.mActivity.startService(serviceIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void stop_service() {
|
|
||||||
Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
|
|
||||||
PythonActivity.mActivity.stopService(serviceIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------------------------------------------
|
|
||||||
// Listener interface for onNewIntent
|
|
||||||
//
|
|
||||||
|
|
||||||
public interface NewIntentListener {
|
|
||||||
void onNewIntent(Intent intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<NewIntentListener> newIntentListeners = null;
|
|
||||||
|
|
||||||
public void registerNewIntentListener(NewIntentListener listener) {
|
|
||||||
if ( this.newIntentListeners == null )
|
|
||||||
this.newIntentListeners = Collections.synchronizedList(new ArrayList<NewIntentListener>());
|
|
||||||
this.newIntentListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterNewIntentListener(NewIntentListener listener) {
|
|
||||||
if ( this.newIntentListeners == null )
|
|
||||||
return;
|
|
||||||
this.newIntentListeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onNewIntent(Intent intent) {
|
|
||||||
if ( this.newIntentListeners == null )
|
|
||||||
return;
|
|
||||||
if ( this.mView != null )
|
|
||||||
this.mView.onResume();
|
|
||||||
synchronized ( this.newIntentListeners ) {
|
|
||||||
Iterator<NewIntentListener> iterator = this.newIntentListeners.iterator();
|
|
||||||
while ( iterator.hasNext() ) {
|
|
||||||
(iterator.next()).onNewIntent(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------------------------------------------
|
|
||||||
// Listener interface for onActivityResult
|
|
||||||
//
|
|
||||||
|
|
||||||
public interface ActivityResultListener {
|
|
||||||
void onActivityResult(int requestCode, int resultCode, Intent data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ActivityResultListener> activityResultListeners = null;
|
|
||||||
|
|
||||||
public void registerActivityResultListener(ActivityResultListener listener) {
|
|
||||||
if ( this.activityResultListeners == null )
|
|
||||||
this.activityResultListeners = Collections.synchronizedList(new ArrayList<ActivityResultListener>());
|
|
||||||
this.activityResultListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterActivityResultListener(ActivityResultListener listener) {
|
|
||||||
if ( this.activityResultListeners == null )
|
|
||||||
return;
|
|
||||||
this.activityResultListeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
|
||||||
if ( this.activityResultListeners == null )
|
|
||||||
return;
|
|
||||||
if ( this.mView != null )
|
|
||||||
this.mView.onResume();
|
|
||||||
synchronized ( this.activityResultListeners ) {
|
|
||||||
Iterator<ActivityResultListener> iterator = this.activityResultListeners.iterator();
|
|
||||||
while ( iterator.hasNext() )
|
|
||||||
(iterator.next()).onActivityResult(requestCode, resultCode, intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------------------------------------------
|
|
||||||
// Billing
|
|
||||||
//
|
|
||||||
class PythonPurchaseObserver extends PurchaseObserver {
|
|
||||||
public PythonPurchaseObserver(Handler handler) {
|
|
||||||
super(PythonActivity.this, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBillingSupported(boolean supported, String type) {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "supported: " + supported);
|
|
||||||
}
|
|
||||||
|
|
||||||
String sup = "1";
|
|
||||||
if ( !supported )
|
|
||||||
sup = "0";
|
|
||||||
if (type == null)
|
|
||||||
type = Consts.ITEM_TYPE_INAPP;
|
|
||||||
|
|
||||||
// add notification for python message queue
|
|
||||||
mActivity.mBillingQueue.add("billingSupported|" + type + "|" + sup);
|
|
||||||
|
|
||||||
// for managed items, restore the database
|
|
||||||
if ( type == Consts.ITEM_TYPE_INAPP && supported ) {
|
|
||||||
restoreDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPurchaseStateChange(PurchaseState purchaseState, String itemId,
|
|
||||||
int quantity, long purchaseTime, String developerPayload) {
|
|
||||||
mActivity.mBillingQueue.add(
|
|
||||||
"purchaseStateChange|" + itemId + "|" + purchaseState.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPurchaseResponse(RequestPurchase request,
|
|
||||||
ResponseCode responseCode) {
|
|
||||||
mActivity.mBillingQueue.add(
|
|
||||||
"requestPurchaseResponse|" + request.mProductId + "|" + responseCode.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRestoreTransactionsResponse(RestoreTransactions request,
|
|
||||||
ResponseCode responseCode) {
|
|
||||||
if (responseCode == ResponseCode.RESULT_OK) {
|
|
||||||
mActivity.mBillingQueue.add("restoreTransaction|ok");
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, "completed RestoreTransactions request");
|
|
||||||
}
|
|
||||||
// Update the shared preferences so that we don't perform
|
|
||||||
// a RestoreTransactions again.
|
|
||||||
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
|
|
||||||
SharedPreferences.Editor edit = prefs.edit();
|
|
||||||
edit.putBoolean(DB_INITIALIZED, true);
|
|
||||||
edit.commit();
|
|
||||||
} else {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, "RestoreTransactions error: " + responseCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
mActivity.mBillingQueue.add(
|
|
||||||
"restoreTransaction|error|" + responseCode.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the database has not been initialized, we send a
|
|
||||||
* RESTORE_TRANSACTIONS request to Android Market to get the list of purchased items
|
|
||||||
* for this user. This happens if the application has just been installed
|
|
||||||
* or the user wiped data. We do not want to do this on every startup, rather, we want to do
|
|
||||||
* only when the database needs to be initialized.
|
|
||||||
*/
|
|
||||||
private void restoreDatabase() {
|
|
||||||
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
|
|
||||||
boolean initialized = prefs.getBoolean(DB_INITIALIZED, false);
|
|
||||||
if (!initialized) {
|
|
||||||
mBillingService.restoreTransactions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An array of product list entries for the products that can be purchased. */
|
|
||||||
|
|
||||||
private enum Managed { MANAGED, UNMANAGED, SUBSCRIPTION }
|
|
||||||
|
|
||||||
|
|
||||||
private PythonPurchaseObserver mPythonPurchaseObserver;
|
|
||||||
private Handler mBillingHandler;
|
|
||||||
private BillingService mBillingService;
|
|
||||||
private PurchaseDatabase mPurchaseDatabase;
|
|
||||||
private String mPayloadContents;
|
|
||||||
public List<String> mBillingQueue;
|
|
||||||
|
|
||||||
public void billingServiceStart_() {
|
|
||||||
mBillingQueue = new ArrayList<String>();
|
|
||||||
|
|
||||||
// Start the billing part
|
|
||||||
mPythonPurchaseObserver = new PythonPurchaseObserver(mBillingHandler);
|
|
||||||
mBillingService = new BillingService();
|
|
||||||
mBillingService.setContext(this);
|
|
||||||
mPurchaseDatabase = new PurchaseDatabase(this);
|
|
||||||
|
|
||||||
ResponseHandler.register(mPythonPurchaseObserver);
|
|
||||||
if (!mBillingService.checkBillingSupported()) {
|
|
||||||
//showDialog(DIALOG_CANNOT_CONNECT_ID);
|
|
||||||
Log.w(TAG, "NO BILLING SUPPORTED");
|
|
||||||
}
|
|
||||||
if (!mBillingService.checkBillingSupported(Consts.ITEM_TYPE_SUBSCRIPTION)) {
|
|
||||||
//showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
|
|
||||||
Log.w(TAG, "NO SUBSCRIPTION SUPPORTED");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void billingServiceStop_() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void billingBuy_(String mSku) {
|
|
||||||
Managed mManagedType = Managed.MANAGED;
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, "buying sku: " + mSku);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mManagedType == Managed.MANAGED) {
|
|
||||||
if (!mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) {
|
|
||||||
Log.w(TAG, "ERROR IN BILLING REQUEST PURCHASE");
|
|
||||||
}
|
|
||||||
} else if (mManagedType == Managed.SUBSCRIPTION) {
|
|
||||||
if (!mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) {
|
|
||||||
Log.w(TAG, "ERROR IN BILLING REQUEST PURCHASE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String billingGetPurchasedItems_() {
|
|
||||||
String ownedItems = "";
|
|
||||||
Cursor cursor = mPurchaseDatabase.queryAllPurchasedItems();
|
|
||||||
if (cursor == null)
|
|
||||||
return "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
int productIdCol = cursor.getColumnIndexOrThrow(
|
|
||||||
PurchaseDatabase.PURCHASED_PRODUCT_ID_COL);
|
|
||||||
int qtCol = cursor.getColumnIndexOrThrow(
|
|
||||||
PurchaseDatabase.PURCHASED_QUANTITY_COL);
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String productId = cursor.getString(productIdCol);
|
|
||||||
String qt = cursor.getString(qtCol);
|
|
||||||
|
|
||||||
productId = Security.unobfuscate(this, Configuration.billing_salt, productId);
|
|
||||||
if ( productId == null )
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if ( ownedItems != "" )
|
|
||||||
ownedItems += "\n";
|
|
||||||
ownedItems += productId + "," + qt;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ownedItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void billingServiceStart() {
|
|
||||||
mActivity.billingServiceStart_();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void billingServiceStop() {
|
|
||||||
mActivity.billingServiceStop_();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void billingBuy(String sku) {
|
|
||||||
mActivity.billingBuy_(sku);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String billingGetPurchasedItems() {
|
|
||||||
return mActivity.billingGetPurchasedItems_();
|
|
||||||
}
|
|
||||||
|
|
||||||
static String billingGetPendingMessage() {
|
|
||||||
if (mActivity.mBillingQueue.isEmpty())
|
|
||||||
return null;
|
|
||||||
return mActivity.mBillingQueue.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,138 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.os.Process;
|
|
||||||
|
|
||||||
public class PythonService extends Service implements Runnable {
|
|
||||||
|
|
||||||
// Thread for Python code
|
|
||||||
private Thread pythonThread = null;
|
|
||||||
|
|
||||||
// Python environment variables
|
|
||||||
private String androidPrivate;
|
|
||||||
private String androidArgument;
|
|
||||||
private String pythonHome;
|
|
||||||
private String pythonPath;
|
|
||||||
// Argument to pass to Python code,
|
|
||||||
private String pythonServiceArgument;
|
|
||||||
public static Service mService = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent arg0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
if (pythonThread != null) {
|
|
||||||
Log.v("python service", "service exists, do not start again");
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bundle extras = intent.getExtras();
|
|
||||||
androidPrivate = extras.getString("androidPrivate");
|
|
||||||
// service code is located in service subdir
|
|
||||||
androidArgument = extras.getString("androidArgument") + "/service";
|
|
||||||
pythonHome = extras.getString("pythonHome");
|
|
||||||
pythonPath = extras.getString("pythonPath");
|
|
||||||
pythonServiceArgument = extras.getString("pythonServiceArgument");
|
|
||||||
String serviceTitle = extras.getString("serviceTitle");
|
|
||||||
String serviceDescription = extras.getString("serviceDescription");
|
|
||||||
|
|
||||||
pythonThread = new Thread(this);
|
|
||||||
pythonThread.start();
|
|
||||||
|
|
||||||
Notification notification;
|
|
||||||
Context context = getApplicationContext();
|
|
||||||
Intent contextIntent = new Intent(context, PythonActivity.class);
|
|
||||||
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
|
||||||
notification = new Notification(
|
|
||||||
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
|
|
||||||
try {
|
|
||||||
// prevent using NotificationCompat, this saves 100kb on apk
|
|
||||||
Method func = notification.getClass().getMethod(
|
|
||||||
"setLatestEventInfo", Context.class, CharSequence.class,
|
|
||||||
CharSequence.class, PendingIntent.class);
|
|
||||||
func.invoke(notification, context, serviceTitle, serviceDescription, pIntent);
|
|
||||||
} catch (NoSuchMethodException | IllegalAccessException |
|
|
||||||
IllegalArgumentException | InvocationTargetException e) {
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Notification.Builder builder = new Notification.Builder(context);
|
|
||||||
builder.setContentTitle(serviceTitle);
|
|
||||||
builder.setContentText(serviceDescription);
|
|
||||||
builder.setContentIntent(pIntent);
|
|
||||||
builder.setSmallIcon(context.getApplicationInfo().icon);
|
|
||||||
notification = builder.build();
|
|
||||||
}
|
|
||||||
startForeground(1, notification);
|
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
pythonThread = null;
|
|
||||||
Process.killProcess(Process.myPid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(){
|
|
||||||
|
|
||||||
// libraries loading, the same way PythonActivity.run() do
|
|
||||||
System.loadLibrary("sdl");
|
|
||||||
System.loadLibrary("sdl_image");
|
|
||||||
System.loadLibrary("sdl_ttf");
|
|
||||||
System.loadLibrary("sdl_mixer");
|
|
||||||
System.loadLibrary("python2.7");
|
|
||||||
System.loadLibrary("application");
|
|
||||||
System.loadLibrary("sdl_main");
|
|
||||||
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_io.so");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/unicodedata.so");
|
|
||||||
|
|
||||||
try {
|
|
||||||
System.loadLibrary("sqlite3");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_sqlite3.so");
|
|
||||||
} catch(UnsatisfiedLinkError e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imaging.so");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imagingft.so");
|
|
||||||
System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imagingmath.so");
|
|
||||||
} catch(UnsatisfiedLinkError e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mService = this;
|
|
||||||
nativeInitJavaEnv();
|
|
||||||
nativeStart(androidPrivate, androidArgument, pythonHome, pythonPath,
|
|
||||||
pythonServiceArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native part
|
|
||||||
public static native void nativeStart(String androidPrivate, String androidArgument,
|
|
||||||
String pythonHome, String pythonPath,
|
|
||||||
String pythonServiceArgument);
|
|
||||||
|
|
||||||
public static native void nativeInitJavaEnv();
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,337 +0,0 @@
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.media.MediaPlayer;
|
|
||||||
import android.util.Log;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
public class RenPySound {
|
|
||||||
|
|
||||||
private static class Channel implements MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener {
|
|
||||||
|
|
||||||
// MediaPlayers for the currently playing and queued up
|
|
||||||
// sounds.
|
|
||||||
MediaPlayer player[];
|
|
||||||
|
|
||||||
// Filenames for the currently playing and queued up sounds.
|
|
||||||
String filename[];
|
|
||||||
|
|
||||||
// Is the corresponding player prepareD?
|
|
||||||
boolean prepared[];
|
|
||||||
|
|
||||||
// The volume for the left and right channel.
|
|
||||||
double volume;
|
|
||||||
double secondary_volume;
|
|
||||||
double left_volume;
|
|
||||||
double right_volume;
|
|
||||||
|
|
||||||
Channel() {
|
|
||||||
player = new MediaPlayer[2];
|
|
||||||
filename = new String[2];
|
|
||||||
prepared = new boolean[2];
|
|
||||||
|
|
||||||
player[0] = new MediaPlayer();
|
|
||||||
player[1] = new MediaPlayer();
|
|
||||||
|
|
||||||
volume = 1.0;
|
|
||||||
secondary_volume = 1.0;
|
|
||||||
left_volume = 1.0;
|
|
||||||
right_volume = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue up a sound file.
|
|
||||||
*/
|
|
||||||
synchronized void queue(String fn, String real_fn, long base, long length) {
|
|
||||||
MediaPlayer mp = player[1];
|
|
||||||
|
|
||||||
mp.reset();
|
|
||||||
|
|
||||||
try {
|
|
||||||
FileInputStream f = new FileInputStream(real_fn);
|
|
||||||
|
|
||||||
if (length >= 0) {
|
|
||||||
mp.setDataSource(f.getFD(), base, length);
|
|
||||||
} else {
|
|
||||||
mp.setDataSource(f.getFD());
|
|
||||||
}
|
|
||||||
|
|
||||||
mp.setOnCompletionListener(this);
|
|
||||||
mp.setOnPreparedListener(this);
|
|
||||||
|
|
||||||
mp.prepareAsync();
|
|
||||||
|
|
||||||
f.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w("RenPySound", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
filename[1] = fn;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play the queued-up sound.
|
|
||||||
*/
|
|
||||||
synchronized void play() {
|
|
||||||
MediaPlayer tmp;
|
|
||||||
|
|
||||||
player[0].reset();
|
|
||||||
|
|
||||||
tmp = player[0];
|
|
||||||
player[0] = player[1];
|
|
||||||
player[1] = tmp;
|
|
||||||
|
|
||||||
filename[0] = filename[1];
|
|
||||||
filename[1] = null;
|
|
||||||
|
|
||||||
prepared[0] = prepared[1];
|
|
||||||
prepared[1] = false;
|
|
||||||
|
|
||||||
if (filename[0] != null) {
|
|
||||||
updateVolume();
|
|
||||||
|
|
||||||
if (prepared[0]) {
|
|
||||||
player[0].start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seek to the position specified on this channel
|
|
||||||
*/
|
|
||||||
|
|
||||||
synchronized void seek(float position) {
|
|
||||||
if (prepared[0]){
|
|
||||||
player[0].seekTo((int)position*1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop playback on this channel.
|
|
||||||
*/
|
|
||||||
synchronized void stop() {
|
|
||||||
player[0].reset();
|
|
||||||
player[1].reset();
|
|
||||||
|
|
||||||
filename[0] = null;
|
|
||||||
filename[1] = null;
|
|
||||||
|
|
||||||
prepared[0] = false;
|
|
||||||
prepared[1] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dequeue the queued file on this channel.
|
|
||||||
*/
|
|
||||||
synchronized void dequeue() {
|
|
||||||
player[1].reset();
|
|
||||||
filename[1] = null;
|
|
||||||
prepared[1] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the volume on the playing file.
|
|
||||||
*/
|
|
||||||
synchronized void updateVolume() {
|
|
||||||
player[0].setVolume((float) (volume * secondary_volume * left_volume),
|
|
||||||
(float) (volume * secondary_volume * right_volume));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update the volume.
|
|
||||||
*/
|
|
||||||
synchronized void set_volume(float v) {
|
|
||||||
volume = v;
|
|
||||||
updateVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update the volume.
|
|
||||||
*/
|
|
||||||
synchronized void set_secondary_volume(float v) {
|
|
||||||
secondary_volume = v;
|
|
||||||
updateVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update the pan. (By setting up the fractional
|
|
||||||
* volume.)
|
|
||||||
*/
|
|
||||||
synchronized void set_pan(float pan) {
|
|
||||||
if (pan < 0) {
|
|
||||||
left_volume = 1.0;
|
|
||||||
right_volume = 1.0 + pan;
|
|
||||||
} else {
|
|
||||||
left_volume = 1.0 - pan;
|
|
||||||
right_volume = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void pause() {
|
|
||||||
if (filename[0] != null) {
|
|
||||||
player[0].pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void unpause() {
|
|
||||||
if (filename[0] != null) {
|
|
||||||
player[0].start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized int get_pos(){
|
|
||||||
if (prepared[0]) {
|
|
||||||
return player[0].getCurrentPosition();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized int get_length(){
|
|
||||||
if (prepared[0]) {
|
|
||||||
return player[0].getDuration();
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized public void onPrepared(MediaPlayer mp) {
|
|
||||||
if (mp == player[0]) {
|
|
||||||
prepared[0] = true;
|
|
||||||
player[0].start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mp == player[1]) {
|
|
||||||
prepared[1] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called on completion.
|
|
||||||
*/
|
|
||||||
synchronized public void onCompletion(MediaPlayer mp) {
|
|
||||||
if (mp == player[0]) {
|
|
||||||
play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// A map from channel number to channel object.
|
|
||||||
static HashMap<Integer, Channel> channels = new HashMap<Integer, Channel>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the Channel object for the numbered channel, returning a
|
|
||||||
* new channel object as necessary.
|
|
||||||
*/
|
|
||||||
static Channel getChannel(int num) {
|
|
||||||
Channel rv = channels.get(num);
|
|
||||||
|
|
||||||
if (rv == null) {
|
|
||||||
rv = new Channel();
|
|
||||||
channels.put(num, rv);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void queue(int channel, String filename, String real_fn, long base, long length) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.queue(filename, real_fn, base, length);
|
|
||||||
if (c.filename[0] == null) {
|
|
||||||
c.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static void play(int channel,
|
|
||||||
String filename,
|
|
||||||
String real_fn,
|
|
||||||
long base,
|
|
||||||
long length) {
|
|
||||||
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.queue(filename, real_fn, base, length);
|
|
||||||
c.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void seek(int channel, float position){
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.seek(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void stop(int channel) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void dequeue(int channel) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.dequeue();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static String playing_name(int channel) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
if (c.filename[0] == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.filename[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
static int queue_depth(int channel) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
|
|
||||||
if (c.filename[0] == null) return 0;
|
|
||||||
if (c.filename[1] == null) return 1;
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_volume(int channel, float v) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.set_volume(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_secondary_volume(int channel, float v) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.set_secondary_volume(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_pan(int channel, float pan) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.set_pan(pan);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void pause(int channel) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void unpause(int channel) {
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
c.unpause();
|
|
||||||
}
|
|
||||||
|
|
||||||
static int get_pos(int channel){
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
return c.get_pos();
|
|
||||||
}
|
|
||||||
|
|
||||||
static int get_length(int channel){
|
|
||||||
Channel c = getChannel(channel);
|
|
||||||
return c.get_length();
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
new MediaPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/**
|
|
||||||
* This class takes care of managing resources for us. In our code, we
|
|
||||||
* can't use R, since the name of the package containing R will
|
|
||||||
* change. (This same code is used in both org.renpy.android and
|
|
||||||
* org.renpy.pygame.) So this is the next best thing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
public class ResourceManager {
|
|
||||||
|
|
||||||
private Activity act;
|
|
||||||
private Resources res;
|
|
||||||
|
|
||||||
public ResourceManager(Activity activity) {
|
|
||||||
act = activity;
|
|
||||||
res = act.getResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getIdentifier(String name, String kind) {
|
|
||||||
return res.getIdentifier(name, kind, act.getPackageName());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getString(String name) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
return res.getString(getIdentifier(name, "string"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public View inflateView(String name) {
|
|
||||||
int id = getIdentifier(name, "layout");
|
|
||||||
return act.getLayoutInflater().inflate(id, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public View getViewById(View v, String name) {
|
|
||||||
int id = getIdentifier(name, "id");
|
|
||||||
return v.findViewById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,120 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android.billing;
|
|
||||||
|
|
||||||
import org.renpy.android.billing.Consts.ResponseCode;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class implements the broadcast receiver for in-app billing. All asynchronous messages from
|
|
||||||
* Android Market come to this app through this receiver. This class forwards all
|
|
||||||
* messages to the {@link BillingService}, which can start background threads,
|
|
||||||
* if necessary, to process the messages. This class runs on the UI thread and must not do any
|
|
||||||
* network I/O, database updates, or any tasks that might take a long time to complete.
|
|
||||||
* It also must not start a background thread because that may be killed as soon as
|
|
||||||
* {@link #onReceive(Context, Intent)} returns.
|
|
||||||
*
|
|
||||||
* You should modify and obfuscate this code before using it.
|
|
||||||
*/
|
|
||||||
public class BillingReceiver extends BroadcastReceiver {
|
|
||||||
private static final String TAG = "BillingReceiver";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the entry point for all asynchronous messages sent from Android Market to
|
|
||||||
* the application. This method forwards the messages on to the
|
|
||||||
* {@link BillingService}, which handles the communication back to Android Market.
|
|
||||||
* The {@link BillingService} also reports state changes back to the application through
|
|
||||||
* the {@link ResponseHandler}.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
String action = intent.getAction();
|
|
||||||
if (Consts.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
|
|
||||||
String signedData = intent.getStringExtra(Consts.INAPP_SIGNED_DATA);
|
|
||||||
String signature = intent.getStringExtra(Consts.INAPP_SIGNATURE);
|
|
||||||
purchaseStateChanged(context, signedData, signature);
|
|
||||||
} else if (Consts.ACTION_NOTIFY.equals(action)) {
|
|
||||||
String notifyId = intent.getStringExtra(Consts.NOTIFICATION_ID);
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "notifyId: " + notifyId);
|
|
||||||
}
|
|
||||||
notify(context, notifyId);
|
|
||||||
} else if (Consts.ACTION_RESPONSE_CODE.equals(action)) {
|
|
||||||
long requestId = intent.getLongExtra(Consts.INAPP_REQUEST_ID, -1);
|
|
||||||
int responseCodeIndex = intent.getIntExtra(Consts.INAPP_RESPONSE_CODE,
|
|
||||||
ResponseCode.RESULT_ERROR.ordinal());
|
|
||||||
checkResponseCode(context, requestId, responseCodeIndex);
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "unexpected action: " + action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when Android Market sends information about a purchase state
|
|
||||||
* change. The signedData parameter is a plaintext JSON string that is
|
|
||||||
* signed by the server with the developer's private key. The signature
|
|
||||||
* for the signed data is passed in the signature parameter.
|
|
||||||
* @param context the context
|
|
||||||
* @param signedData the (unencrypted) JSON string
|
|
||||||
* @param signature the signature for the signedData
|
|
||||||
*/
|
|
||||||
private void purchaseStateChanged(Context context, String signedData, String signature) {
|
|
||||||
Intent intent = new Intent(Consts.ACTION_PURCHASE_STATE_CHANGED);
|
|
||||||
intent.setClass(context, BillingService.class);
|
|
||||||
intent.putExtra(Consts.INAPP_SIGNED_DATA, signedData);
|
|
||||||
intent.putExtra(Consts.INAPP_SIGNATURE, signature);
|
|
||||||
context.startService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when Android Market sends a "notify" message indicating that transaction
|
|
||||||
* information is available. The request includes a nonce (random number used once) that
|
|
||||||
* we generate and Android Market signs and sends back to us with the purchase state and
|
|
||||||
* other transaction details. This BroadcastReceiver cannot bind to the
|
|
||||||
* MarketBillingService directly so it starts the {@link BillingService}, which does the
|
|
||||||
* actual work of sending the message.
|
|
||||||
*
|
|
||||||
* @param context the context
|
|
||||||
* @param notifyId the notification ID
|
|
||||||
*/
|
|
||||||
private void notify(Context context, String notifyId) {
|
|
||||||
Intent intent = new Intent(Consts.ACTION_GET_PURCHASE_INFORMATION);
|
|
||||||
intent.setClass(context, BillingService.class);
|
|
||||||
intent.putExtra(Consts.NOTIFICATION_ID, notifyId);
|
|
||||||
context.startService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when Android Market sends a server response code. The BillingService can
|
|
||||||
* then report the status of the response if desired.
|
|
||||||
*
|
|
||||||
* @param context the context
|
|
||||||
* @param requestId the request ID that corresponds to a previous request
|
|
||||||
* @param responseCodeIndex the ResponseCode ordinal value for the request
|
|
||||||
*/
|
|
||||||
private void checkResponseCode(Context context, long requestId, int responseCodeIndex) {
|
|
||||||
Intent intent = new Intent(Consts.ACTION_RESPONSE_CODE);
|
|
||||||
intent.setClass(context, BillingService.class);
|
|
||||||
intent.putExtra(Consts.INAPP_REQUEST_ID, requestId);
|
|
||||||
intent.putExtra(Consts.INAPP_RESPONSE_CODE, responseCodeIndex);
|
|
||||||
context.startService(intent);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,684 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android.billing;
|
|
||||||
|
|
||||||
import com.android.vending.billing.IMarketBillingService;
|
|
||||||
|
|
||||||
import org.renpy.android.billing.Consts.PurchaseState;
|
|
||||||
import org.renpy.android.billing.Consts.ResponseCode;
|
|
||||||
import org.renpy.android.billing.Security.VerifiedPurchase;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class sends messages to Android Market on behalf of the application by
|
|
||||||
* connecting (binding) to the MarketBillingService. The application
|
|
||||||
* creates an instance of this class and invokes billing requests through this service.
|
|
||||||
*
|
|
||||||
* The {@link BillingReceiver} class starts this service to process commands
|
|
||||||
* that it receives from Android Market.
|
|
||||||
*
|
|
||||||
* You should modify and obfuscate this code before using it.
|
|
||||||
*/
|
|
||||||
public class BillingService extends Service implements ServiceConnection {
|
|
||||||
private static final String TAG = "BillingService";
|
|
||||||
|
|
||||||
/** The service connection to the remote MarketBillingService. */
|
|
||||||
private static IMarketBillingService mService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of requests that are pending while we are waiting for the
|
|
||||||
* connection to the MarketBillingService to be established.
|
|
||||||
*/
|
|
||||||
private static LinkedList<BillingRequest> mPendingRequests = new LinkedList<BillingRequest>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of requests that we have sent to Android Market but for which we have
|
|
||||||
* not yet received a response code. The HashMap is indexed by the
|
|
||||||
* request Id that each request receives when it executes.
|
|
||||||
*/
|
|
||||||
private static HashMap<Long, BillingRequest> mSentRequests =
|
|
||||||
new HashMap<Long, BillingRequest>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base class for all requests that use the MarketBillingService.
|
|
||||||
* Each derived class overrides the run() method to call the appropriate
|
|
||||||
* service interface. If we are already connected to the MarketBillingService,
|
|
||||||
* then we call the run() method directly. Otherwise, we bind
|
|
||||||
* to the service and save the request on a queue to be run later when
|
|
||||||
* the service is connected.
|
|
||||||
*/
|
|
||||||
abstract class BillingRequest {
|
|
||||||
private final int mStartId;
|
|
||||||
protected long mRequestId;
|
|
||||||
|
|
||||||
public BillingRequest(int startId) {
|
|
||||||
mStartId = startId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getStartId() {
|
|
||||||
return mStartId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the request, starting the connection if necessary.
|
|
||||||
* @return true if the request was executed or queued; false if there
|
|
||||||
* was an error starting the connection
|
|
||||||
*/
|
|
||||||
public boolean runRequest() {
|
|
||||||
if (runIfConnected()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bindToMarketBillingService()) {
|
|
||||||
// Add a pending request to run when the service is connected.
|
|
||||||
mPendingRequests.add(this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try running the request directly if the service is already connected.
|
|
||||||
* @return true if the request ran successfully; false if the service
|
|
||||||
* is not connected or there was an error when trying to use it
|
|
||||||
*/
|
|
||||||
public boolean runIfConnected() {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, getClass().getSimpleName());
|
|
||||||
}
|
|
||||||
if (mService != null) {
|
|
||||||
try {
|
|
||||||
mRequestId = run();
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, "request id: " + mRequestId);
|
|
||||||
}
|
|
||||||
if (mRequestId >= 0) {
|
|
||||||
mSentRequests.put(mRequestId, this);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
onRemoteException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a remote exception occurs while trying to execute the
|
|
||||||
* {@link #run()} method. The derived class can override this to
|
|
||||||
* execute exception-handling code.
|
|
||||||
* @param e the exception
|
|
||||||
*/
|
|
||||||
protected void onRemoteException(RemoteException e) {
|
|
||||||
Log.w(TAG, "remote billing service crashed");
|
|
||||||
mService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The derived class must implement this method.
|
|
||||||
* @throws RemoteException
|
|
||||||
*/
|
|
||||||
abstract protected long run() throws RemoteException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when Android Market sends a response code for this
|
|
||||||
* request.
|
|
||||||
* @param responseCode the response code
|
|
||||||
*/
|
|
||||||
protected void responseCodeReceived(ResponseCode responseCode) {
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Bundle makeRequestBundle(String method) {
|
|
||||||
Bundle request = new Bundle();
|
|
||||||
request.putString(Consts.BILLING_REQUEST_METHOD, method);
|
|
||||||
request.putInt(Consts.BILLING_REQUEST_API_VERSION, 2);
|
|
||||||
request.putString(Consts.BILLING_REQUEST_PACKAGE_NAME, getPackageName());
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void logResponseCode(String method, Bundle response) {
|
|
||||||
ResponseCode responseCode = ResponseCode.valueOf(
|
|
||||||
response.getInt(Consts.BILLING_RESPONSE_RESPONSE_CODE));
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.e(TAG, method + " received " + responseCode.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper class that checks if in-app billing is supported.
|
|
||||||
*
|
|
||||||
* Note: Support for subscriptions implies support for one-time purchases. However, the opposite
|
|
||||||
* is not true.
|
|
||||||
*
|
|
||||||
* Developers may want to perform two checks if both one-time and subscription products are
|
|
||||||
* available.
|
|
||||||
*/
|
|
||||||
class CheckBillingSupported extends BillingRequest {
|
|
||||||
public String mProductType = null;
|
|
||||||
|
|
||||||
/** Legacy contrustor
|
|
||||||
*
|
|
||||||
* This constructor is provided for legacy purposes. Assumes the calling application will
|
|
||||||
* not be using any features not present in API v1, such as subscriptions.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public CheckBillingSupported() {
|
|
||||||
// This object is never created as a side effect of starting this
|
|
||||||
// service so we pass -1 as the startId to indicate that we should
|
|
||||||
// not stop this service after executing this request.
|
|
||||||
super(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Constructor
|
|
||||||
*
|
|
||||||
* Note: Support for subscriptions implies support for one-time purchases. However, the
|
|
||||||
* opposite is not true.
|
|
||||||
*
|
|
||||||
* Developers may want to perform two checks if both one-time and subscription products are
|
|
||||||
* available.
|
|
||||||
*
|
|
||||||
* @pram itemType Either Consts.ITEM_TYPE_INAPP or Consts.ITEM_TYPE_SUBSCRIPTION, indicating
|
|
||||||
* the type of item support is being checked for.
|
|
||||||
*/
|
|
||||||
public CheckBillingSupported(String itemType) {
|
|
||||||
super(-1);
|
|
||||||
mProductType = itemType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected long run() throws RemoteException {
|
|
||||||
Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED");
|
|
||||||
if (mProductType != null) {
|
|
||||||
request.putString(Consts.BILLING_REQUEST_ITEM_TYPE, mProductType);
|
|
||||||
}
|
|
||||||
Bundle response = mService.sendBillingRequest(request);
|
|
||||||
int responseCode = response.getInt(Consts.BILLING_RESPONSE_RESPONSE_CODE);
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "CheckBillingSupported response code: " +
|
|
||||||
ResponseCode.valueOf(responseCode));
|
|
||||||
}
|
|
||||||
boolean billingSupported = (responseCode == ResponseCode.RESULT_OK.ordinal());
|
|
||||||
ResponseHandler.checkBillingSupportedResponse(billingSupported, mProductType);
|
|
||||||
return Consts.BILLING_RESPONSE_INVALID_REQUEST_ID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper class that requests a purchase.
|
|
||||||
*/
|
|
||||||
public class RequestPurchase extends BillingRequest {
|
|
||||||
public final String mProductId;
|
|
||||||
public final String mDeveloperPayload;
|
|
||||||
public final String mProductType;
|
|
||||||
|
|
||||||
/** Legacy constructor
|
|
||||||
*
|
|
||||||
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
|
|
||||||
* purchase.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public RequestPurchase(String itemId) {
|
|
||||||
this(itemId, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Legacy constructor
|
|
||||||
*
|
|
||||||
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
|
|
||||||
* purchase.
|
|
||||||
* @param developerPayload Optional data.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public RequestPurchase(String itemId, String developerPayload) {
|
|
||||||
this(itemId, null, developerPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Constructor
|
|
||||||
*
|
|
||||||
* @param itemId The ID of the item to be purchased. Will be assumed to be a one-time
|
|
||||||
* purchase.
|
|
||||||
* @param itemType Either Consts.ITEM_TYPE_INAPP or Consts.ITEM_TYPE_SUBSCRIPTION,
|
|
||||||
* indicating the type of item type support is being checked for.
|
|
||||||
* @param developerPayload Optional data.
|
|
||||||
*/
|
|
||||||
public RequestPurchase(String itemId, String itemType, String developerPayload) {
|
|
||||||
// This object is never created as a side effect of starting this
|
|
||||||
// service so we pass -1 as the startId to indicate that we should
|
|
||||||
// not stop this service after executing this request.
|
|
||||||
super(-1);
|
|
||||||
mProductId = itemId;
|
|
||||||
mDeveloperPayload = developerPayload;
|
|
||||||
mProductType = itemType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected long run() throws RemoteException {
|
|
||||||
Bundle request = makeRequestBundle("REQUEST_PURCHASE");
|
|
||||||
request.putString(Consts.BILLING_REQUEST_ITEM_ID, mProductId);
|
|
||||||
request.putString(Consts.BILLING_REQUEST_ITEM_TYPE, mProductType);
|
|
||||||
// Note that the developer payload is optional.
|
|
||||||
if (mDeveloperPayload != null) {
|
|
||||||
request.putString(Consts.BILLING_REQUEST_DEVELOPER_PAYLOAD, mDeveloperPayload);
|
|
||||||
}
|
|
||||||
Bundle response = mService.sendBillingRequest(request);
|
|
||||||
PendingIntent pendingIntent
|
|
||||||
= response.getParcelable(Consts.BILLING_RESPONSE_PURCHASE_INTENT);
|
|
||||||
if (pendingIntent == null) {
|
|
||||||
Log.e(TAG, "Error with requestPurchase");
|
|
||||||
return Consts.BILLING_RESPONSE_INVALID_REQUEST_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent();
|
|
||||||
ResponseHandler.buyPageIntentResponse(pendingIntent, intent);
|
|
||||||
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
|
|
||||||
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void responseCodeReceived(ResponseCode responseCode) {
|
|
||||||
ResponseHandler.responseCodeReceived(BillingService.this, this, responseCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper class that confirms a list of notifications to the server.
|
|
||||||
*/
|
|
||||||
public class ConfirmNotifications extends BillingRequest {
|
|
||||||
final String[] mNotifyIds;
|
|
||||||
|
|
||||||
public ConfirmNotifications(int startId, String[] notifyIds) {
|
|
||||||
super(startId);
|
|
||||||
mNotifyIds = notifyIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected long run() throws RemoteException {
|
|
||||||
Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS");
|
|
||||||
request.putStringArray(Consts.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
|
|
||||||
Bundle response = mService.sendBillingRequest(request);
|
|
||||||
logResponseCode("confirmNotifications", response);
|
|
||||||
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
|
|
||||||
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper class that sends a GET_PURCHASE_INFORMATION message to the server.
|
|
||||||
*/
|
|
||||||
public class GetPurchaseInformation extends BillingRequest {
|
|
||||||
long mNonce;
|
|
||||||
final String[] mNotifyIds;
|
|
||||||
|
|
||||||
public GetPurchaseInformation(int startId, String[] notifyIds) {
|
|
||||||
super(startId);
|
|
||||||
mNotifyIds = notifyIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected long run() throws RemoteException {
|
|
||||||
mNonce = Security.generateNonce();
|
|
||||||
|
|
||||||
Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION");
|
|
||||||
request.putLong(Consts.BILLING_REQUEST_NONCE, mNonce);
|
|
||||||
request.putStringArray(Consts.BILLING_REQUEST_NOTIFY_IDS, mNotifyIds);
|
|
||||||
Bundle response = mService.sendBillingRequest(request);
|
|
||||||
logResponseCode("getPurchaseInformation", response);
|
|
||||||
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
|
|
||||||
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRemoteException(RemoteException e) {
|
|
||||||
super.onRemoteException(e);
|
|
||||||
Security.removeNonce(mNonce);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper class that sends a RESTORE_TRANSACTIONS message to the server.
|
|
||||||
*/
|
|
||||||
public class RestoreTransactions extends BillingRequest {
|
|
||||||
long mNonce;
|
|
||||||
|
|
||||||
public RestoreTransactions() {
|
|
||||||
// This object is never created as a side effect of starting this
|
|
||||||
// service so we pass -1 as the startId to indicate that we should
|
|
||||||
// not stop this service after executing this request.
|
|
||||||
super(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected long run() throws RemoteException {
|
|
||||||
mNonce = Security.generateNonce();
|
|
||||||
|
|
||||||
Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS");
|
|
||||||
request.putLong(Consts.BILLING_REQUEST_NONCE, mNonce);
|
|
||||||
Bundle response = mService.sendBillingRequest(request);
|
|
||||||
logResponseCode("restoreTransactions", response);
|
|
||||||
return response.getLong(Consts.BILLING_RESPONSE_REQUEST_ID,
|
|
||||||
Consts.BILLING_RESPONSE_INVALID_REQUEST_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRemoteException(RemoteException e) {
|
|
||||||
super.onRemoteException(e);
|
|
||||||
Security.removeNonce(mNonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void responseCodeReceived(ResponseCode responseCode) {
|
|
||||||
ResponseHandler.responseCodeReceived(BillingService.this, this, responseCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public BillingService() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContext(Context context) {
|
|
||||||
attachBaseContext(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We don't support binding to this service, only starting the service.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart(Intent intent, int startId) {
|
|
||||||
handleCommand(intent, startId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link BillingReceiver} sends messages to this service using intents.
|
|
||||||
* Each intent has an action and some extra arguments specific to that action.
|
|
||||||
* @param intent the intent containing one of the supported actions
|
|
||||||
* @param startId an identifier for the invocation instance of this service
|
|
||||||
*/
|
|
||||||
public void handleCommand(Intent intent, int startId) {
|
|
||||||
String action = intent.getAction();
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "handleCommand() action: " + action);
|
|
||||||
}
|
|
||||||
if (Consts.ACTION_CONFIRM_NOTIFICATION.equals(action)) {
|
|
||||||
String[] notifyIds = intent.getStringArrayExtra(Consts.NOTIFICATION_ID);
|
|
||||||
confirmNotifications(startId, notifyIds);
|
|
||||||
} else if (Consts.ACTION_GET_PURCHASE_INFORMATION.equals(action)) {
|
|
||||||
String notifyId = intent.getStringExtra(Consts.NOTIFICATION_ID);
|
|
||||||
getPurchaseInformation(startId, new String[] { notifyId });
|
|
||||||
} else if (Consts.ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
|
|
||||||
String signedData = intent.getStringExtra(Consts.INAPP_SIGNED_DATA);
|
|
||||||
String signature = intent.getStringExtra(Consts.INAPP_SIGNATURE);
|
|
||||||
purchaseStateChanged(startId, signedData, signature);
|
|
||||||
} else if (Consts.ACTION_RESPONSE_CODE.equals(action)) {
|
|
||||||
long requestId = intent.getLongExtra(Consts.INAPP_REQUEST_ID, -1);
|
|
||||||
int responseCodeIndex = intent.getIntExtra(Consts.INAPP_RESPONSE_CODE,
|
|
||||||
ResponseCode.RESULT_ERROR.ordinal());
|
|
||||||
ResponseCode responseCode = ResponseCode.valueOf(responseCodeIndex);
|
|
||||||
checkResponseCode(requestId, responseCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds to the MarketBillingService and returns true if the bind
|
|
||||||
* succeeded.
|
|
||||||
* @return true if the bind succeeded; false otherwise
|
|
||||||
*/
|
|
||||||
private boolean bindToMarketBillingService() {
|
|
||||||
try {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "binding to Market billing service");
|
|
||||||
}
|
|
||||||
boolean bindResult = bindService(
|
|
||||||
new Intent(Consts.MARKET_BILLING_SERVICE_ACTION),
|
|
||||||
this, // ServiceConnection.
|
|
||||||
Context.BIND_AUTO_CREATE);
|
|
||||||
|
|
||||||
if (bindResult) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Could not bind to service.");
|
|
||||||
}
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Log.e(TAG, "Security exception: " + e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if in-app billing is supported. Assumes this is a one-time purchase.
|
|
||||||
*
|
|
||||||
* @return true if supported; false otherwise
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean checkBillingSupported() {
|
|
||||||
return new CheckBillingSupported().runRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if in-app billing is supported.
|
|
||||||
* @pram itemType Either Consts.ITEM_TYPE_INAPP or Consts.ITEM_TYPE_SUBSCRIPTION, indicating the
|
|
||||||
* type of item support is being checked for.
|
|
||||||
* @return true if supported; false otherwise
|
|
||||||
*/
|
|
||||||
public boolean checkBillingSupported(String itemType) {
|
|
||||||
return new CheckBillingSupported(itemType).runRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests that the given item be offered to the user for purchase. When
|
|
||||||
* the purchase succeeds (or is canceled) the {@link BillingReceiver}
|
|
||||||
* receives an intent with the action {@link Consts#ACTION_NOTIFY}.
|
|
||||||
* Returns false if there was an error trying to connect to Android Market.
|
|
||||||
* @param productId an identifier for the item being offered for purchase
|
|
||||||
* @param itemType Either Consts.ITEM_TYPE_INAPP or Consts.ITEM_TYPE_SUBSCRIPTION, indicating
|
|
||||||
* the type of item type support is being checked for.
|
|
||||||
* @param developerPayload a payload that is associated with a given
|
|
||||||
* purchase, if null, no payload is sent
|
|
||||||
* @return false if there was an error connecting to Android Market
|
|
||||||
*/
|
|
||||||
public boolean requestPurchase(String productId, String itemType, String developerPayload) {
|
|
||||||
return new RequestPurchase(productId, itemType, developerPayload).runRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests transaction information for all managed items. Call this only when the
|
|
||||||
* application is first installed or after a database wipe. Do NOT call this
|
|
||||||
* every time the application starts up.
|
|
||||||
* @return false if there was an error connecting to Android Market
|
|
||||||
*/
|
|
||||||
public boolean restoreTransactions() {
|
|
||||||
return new RestoreTransactions().runRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirms receipt of a purchase state change. Each {@code notifyId} is
|
|
||||||
* an opaque identifier that came from the server. This method sends those
|
|
||||||
* identifiers back to the MarketBillingService, which ACKs them to the
|
|
||||||
* server. Returns false if there was an error trying to connect to the
|
|
||||||
* MarketBillingService.
|
|
||||||
* @param startId an identifier for the invocation instance of this service
|
|
||||||
* @param notifyIds a list of opaque identifiers associated with purchase
|
|
||||||
* state changes.
|
|
||||||
* @return false if there was an error connecting to Market
|
|
||||||
*/
|
|
||||||
private boolean confirmNotifications(int startId, String[] notifyIds) {
|
|
||||||
return new ConfirmNotifications(startId, notifyIds).runRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the purchase information. This message includes a list of
|
|
||||||
* notification IDs sent to us by Android Market, which we include in
|
|
||||||
* our request. The server responds with the purchase information,
|
|
||||||
* encoded as a JSON string, and sends that to the {@link BillingReceiver}
|
|
||||||
* in an intent with the action {@link Consts#ACTION_PURCHASE_STATE_CHANGED}.
|
|
||||||
* Returns false if there was an error trying to connect to the MarketBillingService.
|
|
||||||
*
|
|
||||||
* @param startId an identifier for the invocation instance of this service
|
|
||||||
* @param notifyIds a list of opaque identifiers associated with purchase
|
|
||||||
* state changes
|
|
||||||
* @return false if there was an error connecting to Android Market
|
|
||||||
*/
|
|
||||||
private boolean getPurchaseInformation(int startId, String[] notifyIds) {
|
|
||||||
return new GetPurchaseInformation(startId, notifyIds).runRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the data was signed with the given signature, and calls
|
|
||||||
* {@link ResponseHandler#purchaseResponse(Context, PurchaseState, String, String, long)}
|
|
||||||
* for each verified purchase.
|
|
||||||
* @param startId an identifier for the invocation instance of this service
|
|
||||||
* @param signedData the signed JSON string (signed, not encrypted)
|
|
||||||
* @param signature the signature for the data, signed with the private key
|
|
||||||
*/
|
|
||||||
private void purchaseStateChanged(int startId, String signedData, String signature) {
|
|
||||||
ArrayList<Security.VerifiedPurchase> purchases;
|
|
||||||
purchases = Security.verifyPurchase(signedData, signature);
|
|
||||||
if (purchases == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, "purchaseStateChanged: " + signedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<String> notifyList = new ArrayList<String>();
|
|
||||||
for (VerifiedPurchase vp : purchases) {
|
|
||||||
if (vp.notificationId != null) {
|
|
||||||
notifyList.add(vp.notificationId);
|
|
||||||
}
|
|
||||||
ResponseHandler.purchaseResponse(this, vp.purchaseState, vp.productId,
|
|
||||||
vp.orderId, vp.purchaseTime, vp.developerPayload,
|
|
||||||
vp.purchaseToken, vp.packageName);
|
|
||||||
}
|
|
||||||
if (!notifyList.isEmpty()) {
|
|
||||||
String[] notifyIds = notifyList.toArray(new String[notifyList.size()]);
|
|
||||||
confirmNotifications(startId, notifyIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when we receive a response code from Android Market for a request
|
|
||||||
* that we made. This is used for reporting various errors and for
|
|
||||||
* acknowledging that an order was sent to the server. This is NOT used
|
|
||||||
* for any purchase state changes. All purchase state changes are received
|
|
||||||
* in the {@link BillingReceiver} and passed to this service, where they are
|
|
||||||
* handled in {@link #purchaseStateChanged(int, String, String)}.
|
|
||||||
* @param requestId a number that identifies a request, assigned at the
|
|
||||||
* time the request was made to Android Market
|
|
||||||
* @param responseCode a response code from Android Market to indicate the state
|
|
||||||
* of the request
|
|
||||||
*/
|
|
||||||
private void checkResponseCode(long requestId, ResponseCode responseCode) {
|
|
||||||
BillingRequest request = mSentRequests.get(requestId);
|
|
||||||
if (request != null) {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, request.getClass().getSimpleName() + ": " + responseCode);
|
|
||||||
}
|
|
||||||
request.responseCodeReceived(responseCode);
|
|
||||||
}
|
|
||||||
mSentRequests.remove(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs any pending requests that are waiting for a connection to the
|
|
||||||
* service to be established. This runs in the main UI thread.
|
|
||||||
*/
|
|
||||||
private void runPendingRequests() {
|
|
||||||
int maxStartId = -1;
|
|
||||||
BillingRequest request;
|
|
||||||
while ((request = mPendingRequests.peek()) != null) {
|
|
||||||
if (request.runIfConnected()) {
|
|
||||||
// Remove the request
|
|
||||||
mPendingRequests.remove();
|
|
||||||
|
|
||||||
// Remember the largest startId, which is the most recent
|
|
||||||
// request to start this service.
|
|
||||||
if (maxStartId < request.getStartId()) {
|
|
||||||
maxStartId = request.getStartId();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The service crashed, so restart it. Note that this leaves
|
|
||||||
// the current request on the queue.
|
|
||||||
bindToMarketBillingService();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here then all the requests ran successfully. If maxStartId
|
|
||||||
// is not -1, then one of the requests started the service, so we can
|
|
||||||
// stop it now.
|
|
||||||
if (maxStartId >= 0) {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "stopping service, startId: " + maxStartId);
|
|
||||||
}
|
|
||||||
stopSelf(maxStartId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when we are connected to the MarketBillingService.
|
|
||||||
* This runs in the main UI thread.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
||||||
if (Consts.DEBUG)
|
|
||||||
Log.d(TAG, "Billing service connected");
|
|
||||||
mService = IMarketBillingService.Stub.asInterface(service);
|
|
||||||
runPendingRequests();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when we are disconnected from the MarketBillingService.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
if (Consts.DEBUG)
|
|
||||||
Log.w(TAG, "Billing service disconnected");
|
|
||||||
mService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unbinds from the MarketBillingService. Call this when the application
|
|
||||||
* terminates to avoid leaking a ServiceConnection.
|
|
||||||
*/
|
|
||||||
public void unbind() {
|
|
||||||
try {
|
|
||||||
unbindService(this);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// This might happen if the service was disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android.billing;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class holds global constants that are used throughout the application
|
|
||||||
* to support in-app billing.
|
|
||||||
*/
|
|
||||||
public class Consts {
|
|
||||||
// The response codes for a request, defined by Android Market.
|
|
||||||
public enum ResponseCode {
|
|
||||||
RESULT_OK,
|
|
||||||
RESULT_USER_CANCELED,
|
|
||||||
RESULT_SERVICE_UNAVAILABLE,
|
|
||||||
RESULT_BILLING_UNAVAILABLE,
|
|
||||||
RESULT_ITEM_UNAVAILABLE,
|
|
||||||
RESULT_DEVELOPER_ERROR,
|
|
||||||
RESULT_ERROR;
|
|
||||||
|
|
||||||
// Converts from an ordinal value to the ResponseCode
|
|
||||||
public static ResponseCode valueOf(int index) {
|
|
||||||
ResponseCode[] values = ResponseCode.values();
|
|
||||||
if (index < 0 || index >= values.length) {
|
|
||||||
return RESULT_ERROR;
|
|
||||||
}
|
|
||||||
return values[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The possible states of an in-app purchase, as defined by Android Market.
|
|
||||||
public enum PurchaseState {
|
|
||||||
// Responses to requestPurchase or restoreTransactions.
|
|
||||||
PURCHASED, // User was charged for the order.
|
|
||||||
CANCELED, // The charge failed on the server.
|
|
||||||
REFUNDED; // User received a refund for the order.
|
|
||||||
|
|
||||||
// Converts from an ordinal value to the PurchaseState
|
|
||||||
public static PurchaseState valueOf(int index) {
|
|
||||||
PurchaseState[] values = PurchaseState.values();
|
|
||||||
if (index < 0 || index >= values.length) {
|
|
||||||
return CANCELED;
|
|
||||||
}
|
|
||||||
return values[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This is the action we use to bind to the MarketBillingService. */
|
|
||||||
public static final String MARKET_BILLING_SERVICE_ACTION =
|
|
||||||
"com.android.vending.billing.MarketBillingService.BIND";
|
|
||||||
|
|
||||||
// Intent actions that we send from the BillingReceiver to the
|
|
||||||
// BillingService. Defined by this application.
|
|
||||||
public static final String ACTION_CONFIRM_NOTIFICATION =
|
|
||||||
"com.example.subscriptions.CONFIRM_NOTIFICATION";
|
|
||||||
public static final String ACTION_GET_PURCHASE_INFORMATION =
|
|
||||||
"com.example.subscriptions.GET_PURCHASE_INFORMATION";
|
|
||||||
public static final String ACTION_RESTORE_TRANSACTIONS =
|
|
||||||
"com.example.subscriptions.RESTORE_TRANSACTIONS";
|
|
||||||
|
|
||||||
// Intent actions that we receive in the BillingReceiver from Market.
|
|
||||||
// These are defined by Market and cannot be changed.
|
|
||||||
public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY";
|
|
||||||
public static final String ACTION_RESPONSE_CODE =
|
|
||||||
"com.android.vending.billing.RESPONSE_CODE";
|
|
||||||
public static final String ACTION_PURCHASE_STATE_CHANGED =
|
|
||||||
"com.android.vending.billing.PURCHASE_STATE_CHANGED";
|
|
||||||
|
|
||||||
// These are the names of the extras that are passed in an intent from
|
|
||||||
// Market to this application and cannot be changed.
|
|
||||||
public static final String NOTIFICATION_ID = "notification_id";
|
|
||||||
public static final String INAPP_SIGNED_DATA = "inapp_signed_data";
|
|
||||||
public static final String INAPP_SIGNATURE = "inapp_signature";
|
|
||||||
public static final String INAPP_REQUEST_ID = "request_id";
|
|
||||||
public static final String INAPP_RESPONSE_CODE = "response_code";
|
|
||||||
|
|
||||||
// These are the names of the fields in the request bundle.
|
|
||||||
public static final String BILLING_REQUEST_METHOD = "BILLING_REQUEST";
|
|
||||||
public static final String BILLING_REQUEST_API_VERSION = "API_VERSION";
|
|
||||||
public static final String BILLING_REQUEST_PACKAGE_NAME = "PACKAGE_NAME";
|
|
||||||
public static final String BILLING_REQUEST_ITEM_ID = "ITEM_ID";
|
|
||||||
public static final String BILLING_REQUEST_ITEM_TYPE = "ITEM_TYPE";
|
|
||||||
public static final String BILLING_REQUEST_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD";
|
|
||||||
public static final String BILLING_REQUEST_NOTIFY_IDS = "NOTIFY_IDS";
|
|
||||||
public static final String BILLING_REQUEST_NONCE = "NONCE";
|
|
||||||
|
|
||||||
public static final String BILLING_RESPONSE_RESPONSE_CODE = "RESPONSE_CODE";
|
|
||||||
public static final String BILLING_RESPONSE_PURCHASE_INTENT = "PURCHASE_INTENT";
|
|
||||||
public static final String BILLING_RESPONSE_REQUEST_ID = "REQUEST_ID";
|
|
||||||
public static long BILLING_RESPONSE_INVALID_REQUEST_ID = -1;
|
|
||||||
|
|
||||||
// These are the types supported in the IAB v2
|
|
||||||
public static final String ITEM_TYPE_INAPP = "inapp";
|
|
||||||
public static final String ITEM_TYPE_SUBSCRIPTION = "subs";
|
|
||||||
|
|
||||||
public static final boolean DEBUG = false;
|
|
||||||
}
|
|
|
@ -1,209 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android.billing;
|
|
||||||
|
|
||||||
import org.renpy.android.billing.Consts.PurchaseState;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An example database that records the state of each purchase. You should use
|
|
||||||
* an obfuscator before storing any information to persistent storage. The
|
|
||||||
* obfuscator should use a key that is specific to the device and/or user.
|
|
||||||
* Otherwise an attacker could copy a database full of valid purchases and
|
|
||||||
* distribute it to others.
|
|
||||||
*/
|
|
||||||
public class PurchaseDatabase {
|
|
||||||
private static final String TAG = "PurchaseDatabase";
|
|
||||||
private static final String DATABASE_NAME = "purchase.db";
|
|
||||||
private static final int DATABASE_VERSION = 1;
|
|
||||||
private static final String PURCHASE_HISTORY_TABLE_NAME = "history";
|
|
||||||
private static final String PURCHASED_ITEMS_TABLE_NAME = "purchased";
|
|
||||||
|
|
||||||
// These are the column names for the purchase history table. We need a
|
|
||||||
// column named "_id" if we want to use a CursorAdapter. The primary key is
|
|
||||||
// the orderId so that we can be robust against getting multiple messages
|
|
||||||
// from the server for the same purchase.
|
|
||||||
static final String HISTORY_ORDER_ID_COL = "_id";
|
|
||||||
static final String HISTORY_STATE_COL = "state";
|
|
||||||
static final String HISTORY_PRODUCT_ID_COL = "productId";
|
|
||||||
static final String HISTORY_PURCHASE_TIME_COL = "purchaseTime";
|
|
||||||
static final String HISTORY_DEVELOPER_PAYLOAD_COL = "developerPayload";
|
|
||||||
|
|
||||||
private static final String[] HISTORY_COLUMNS = {
|
|
||||||
HISTORY_ORDER_ID_COL, HISTORY_PRODUCT_ID_COL, HISTORY_STATE_COL,
|
|
||||||
HISTORY_PURCHASE_TIME_COL, HISTORY_DEVELOPER_PAYLOAD_COL
|
|
||||||
};
|
|
||||||
|
|
||||||
// These are the column names for the "purchased items" table.
|
|
||||||
static public final String PURCHASED_PRODUCT_ID_COL = "_id";
|
|
||||||
static public final String PURCHASED_QUANTITY_COL = "quantity";
|
|
||||||
|
|
||||||
private static final String[] PURCHASED_COLUMNS = {
|
|
||||||
PURCHASED_PRODUCT_ID_COL, PURCHASED_QUANTITY_COL
|
|
||||||
};
|
|
||||||
|
|
||||||
private SQLiteDatabase mDb;
|
|
||||||
private DatabaseHelper mDatabaseHelper;
|
|
||||||
|
|
||||||
public PurchaseDatabase(Context context) {
|
|
||||||
mDatabaseHelper = new DatabaseHelper(context);
|
|
||||||
mDb = mDatabaseHelper.getWritableDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
mDatabaseHelper.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a purchased product into the database. There may be multiple
|
|
||||||
* rows in the table for the same product if it was purchased multiple times
|
|
||||||
* or if it was refunded.
|
|
||||||
* @param orderId the order ID (matches the value in the product list)
|
|
||||||
* @param productId the product ID (sku)
|
|
||||||
* @param state the state of the purchase
|
|
||||||
* @param purchaseTime the purchase time (in milliseconds since the epoch)
|
|
||||||
* @param developerPayload the developer provided "payload" associated with
|
|
||||||
* the order.
|
|
||||||
*/
|
|
||||||
private void insertOrder(String orderId, String productId, PurchaseState state,
|
|
||||||
long purchaseTime, String developerPayload) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(HISTORY_ORDER_ID_COL, orderId);
|
|
||||||
values.put(HISTORY_PRODUCT_ID_COL, productId);
|
|
||||||
values.put(HISTORY_STATE_COL, state.ordinal());
|
|
||||||
values.put(HISTORY_PURCHASE_TIME_COL, purchaseTime);
|
|
||||||
values.put(HISTORY_DEVELOPER_PAYLOAD_COL, developerPayload);
|
|
||||||
mDb.replace(PURCHASE_HISTORY_TABLE_NAME, null /* nullColumnHack */, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the quantity of the given product to the given value. If the
|
|
||||||
* given value is zero, then the product is removed from the table.
|
|
||||||
* @param productId the product to update
|
|
||||||
* @param quantity the number of times the product has been purchased
|
|
||||||
*/
|
|
||||||
private void updatePurchasedItem(String productId, int quantity) {
|
|
||||||
if (quantity == 0) {
|
|
||||||
mDb.delete(PURCHASED_ITEMS_TABLE_NAME, PURCHASED_PRODUCT_ID_COL + "=?",
|
|
||||||
new String[] { productId });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(PURCHASED_PRODUCT_ID_COL, productId);
|
|
||||||
values.put(PURCHASED_QUANTITY_COL, quantity);
|
|
||||||
mDb.replace(PURCHASED_ITEMS_TABLE_NAME, null /* nullColumnHack */, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given purchase information to the database and returns the total
|
|
||||||
* number of times that the given product has been purchased.
|
|
||||||
* @param orderId a string identifying the order
|
|
||||||
* @param productId the product ID (sku)
|
|
||||||
* @param purchaseState the purchase state of the product
|
|
||||||
* @param purchaseTime the time the product was purchased, in milliseconds
|
|
||||||
* since the epoch (Jan 1, 1970)
|
|
||||||
* @param developerPayload the developer provided "payload" associated with
|
|
||||||
* the order
|
|
||||||
* @return the number of times the given product has been purchased.
|
|
||||||
*/
|
|
||||||
public synchronized int updatePurchase(String orderId, String productId,
|
|
||||||
PurchaseState purchaseState, long purchaseTime, String developerPayload) {
|
|
||||||
insertOrder(orderId, productId, purchaseState, purchaseTime, developerPayload);
|
|
||||||
Cursor cursor = mDb.query(PURCHASE_HISTORY_TABLE_NAME, HISTORY_COLUMNS,
|
|
||||||
HISTORY_PRODUCT_ID_COL + "=?", new String[] { productId }, null, null, null, null);
|
|
||||||
if (cursor == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
int quantity = 0;
|
|
||||||
try {
|
|
||||||
// Count the number of times the product was purchased
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
int stateIndex = cursor.getInt(2);
|
|
||||||
PurchaseState state = PurchaseState.valueOf(stateIndex);
|
|
||||||
// Note that a refunded purchase is treated as a purchase. Such
|
|
||||||
// a friendly refund policy is nice for the user.
|
|
||||||
if (state == PurchaseState.PURCHASED || state == PurchaseState.REFUNDED) {
|
|
||||||
quantity += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the "purchased items" table
|
|
||||||
updatePurchasedItem(productId, quantity);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a cursor that can be used to read all the rows and columns of
|
|
||||||
* the "purchased items" table.
|
|
||||||
*/
|
|
||||||
public Cursor queryAllPurchasedItems() {
|
|
||||||
return mDb.query(PURCHASED_ITEMS_TABLE_NAME, PURCHASED_COLUMNS, null,
|
|
||||||
null, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a standard helper class for constructing the database.
|
|
||||||
*/
|
|
||||||
private class DatabaseHelper extends SQLiteOpenHelper {
|
|
||||||
public DatabaseHelper(Context context) {
|
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(SQLiteDatabase db) {
|
|
||||||
createPurchaseTable(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
||||||
// Production-quality upgrade code should modify the tables when
|
|
||||||
// the database version changes instead of dropping the tables and
|
|
||||||
// re-creating them.
|
|
||||||
if (newVersion != DATABASE_VERSION) {
|
|
||||||
Log.w(TAG, "Database upgrade from old: " + oldVersion + " to: " +
|
|
||||||
newVersion);
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + PURCHASE_HISTORY_TABLE_NAME);
|
|
||||||
db.execSQL("DROP TABLE IF EXISTS " + PURCHASED_ITEMS_TABLE_NAME);
|
|
||||||
createPurchaseTable(db);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createPurchaseTable(SQLiteDatabase db) {
|
|
||||||
db.execSQL("CREATE TABLE " + PURCHASE_HISTORY_TABLE_NAME + "(" +
|
|
||||||
HISTORY_ORDER_ID_COL + " TEXT PRIMARY KEY, " +
|
|
||||||
HISTORY_STATE_COL + " INTEGER, " +
|
|
||||||
HISTORY_PRODUCT_ID_COL + " TEXT, " +
|
|
||||||
HISTORY_DEVELOPER_PAYLOAD_COL + " TEXT, " +
|
|
||||||
HISTORY_PURCHASE_TIME_COL + " INTEGER)");
|
|
||||||
db.execSQL("CREATE TABLE " + PURCHASED_ITEMS_TABLE_NAME + "(" +
|
|
||||||
PURCHASED_PRODUCT_ID_COL + " TEXT PRIMARY KEY, " +
|
|
||||||
PURCHASED_QUANTITY_COL + " INTEGER)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
|
||||||
|
|
||||||
package org.renpy.android.billing;
|
|
||||||
|
|
||||||
import org.renpy.android.billing.BillingService.RequestPurchase;
|
|
||||||
import org.renpy.android.billing.BillingService.RestoreTransactions;
|
|
||||||
import org.renpy.android.billing.Consts.PurchaseState;
|
|
||||||
import org.renpy.android.billing.Consts.ResponseCode;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.PendingIntent.CanceledException;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentSender;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface for observing changes related to purchases. The main application
|
|
||||||
* extends this class and registers an instance of that derived class with
|
|
||||||
* {@link ResponseHandler}. The main application implements the callbacks
|
|
||||||
* {@link #onBillingSupported(boolean)} and
|
|
||||||
* {@link #onPurchaseStateChange(PurchaseState, String, int, long)}. These methods
|
|
||||||
* are used to update the UI.
|
|
||||||
*/
|
|
||||||
public abstract class PurchaseObserver {
|
|
||||||
private static final String TAG = "PurchaseObserver";
|
|
||||||
private final Activity mActivity;
|
|
||||||
private final Handler mHandler;
|
|
||||||
private Method mStartIntentSender;
|
|
||||||
private Object[] mStartIntentSenderArgs = new Object[5];
|
|
||||||
private static final Class[] START_INTENT_SENDER_SIG = new Class[] {
|
|
||||||
IntentSender.class, Intent.class, int.class, int.class, int.class
|
|
||||||
};
|
|
||||||
|
|
||||||
public PurchaseObserver(Activity activity, Handler handler) {
|
|
||||||
mActivity = activity;
|
|
||||||
mHandler = handler;
|
|
||||||
initCompatibilityLayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the callback that is invoked when Android Market responds to the
|
|
||||||
* {@link BillingService#checkBillingSupported()} request.
|
|
||||||
* @param supported true if in-app billing is supported.
|
|
||||||
*/
|
|
||||||
public abstract void onBillingSupported(boolean supported, String type);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the callback that is invoked when an item is purchased,
|
|
||||||
* refunded, or canceled. It is the callback invoked in response to
|
|
||||||
* calling {@link BillingService#requestPurchase(String)}. It may also
|
|
||||||
* be invoked asynchronously when a purchase is made on another device
|
|
||||||
* (if the purchase was for a Market-managed item), or if the purchase
|
|
||||||
* was refunded, or the charge was canceled. This handles the UI
|
|
||||||
* update. The database update is handled in
|
|
||||||
* {@link ResponseHandler#purchaseResponse(Context, PurchaseState,
|
|
||||||
* String, String, long)}.
|
|
||||||
* @param purchaseState the purchase state of the item
|
|
||||||
* @param itemId a string identifying the item (the "SKU")
|
|
||||||
* @param quantity the current quantity of this item after the purchase
|
|
||||||
* @param purchaseTime the time the product was purchased, in
|
|
||||||
* milliseconds since the epoch (Jan 1, 1970)
|
|
||||||
*/
|
|
||||||
public abstract void onPurchaseStateChange(PurchaseState purchaseState,
|
|
||||||
String itemId, int quantity, long purchaseTime, String developerPayload);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when we receive a response code from Market for a
|
|
||||||
* RequestPurchase request that we made. This is NOT used for any
|
|
||||||
* purchase state changes. All purchase state changes are received in
|
|
||||||
* {@link #onPurchaseStateChange(PurchaseState, String, int, long)}.
|
|
||||||
* This is used for reporting various errors, or if the user backed out
|
|
||||||
* and didn't purchase the item. The possible response codes are:
|
|
||||||
* RESULT_OK means that the order was sent successfully to the server.
|
|
||||||
* The onPurchaseStateChange() will be invoked later (with a
|
|
||||||
* purchase state of PURCHASED or CANCELED) when the order is
|
|
||||||
* charged or canceled. This response code can also happen if an
|
|
||||||
* order for a Market-managed item was already sent to the server.
|
|
||||||
* RESULT_USER_CANCELED means that the user didn't buy the item.
|
|
||||||
* RESULT_SERVICE_UNAVAILABLE means that we couldn't connect to the
|
|
||||||
* Android Market server (for example if the data connection is down).
|
|
||||||
* RESULT_BILLING_UNAVAILABLE means that in-app billing is not
|
|
||||||
* supported yet.
|
|
||||||
* RESULT_ITEM_UNAVAILABLE means that the item this app offered for
|
|
||||||
* sale does not exist (or is not published) in the server-side
|
|
||||||
* catalog.
|
|
||||||
* RESULT_ERROR is used for any other errors (such as a server error).
|
|
||||||
*/
|
|
||||||
public abstract void onRequestPurchaseResponse(RequestPurchase request,
|
|
||||||
ResponseCode responseCode);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when we receive a response code from Android Market for a
|
|
||||||
* RestoreTransactions request that we made. A response code of
|
|
||||||
* RESULT_OK means that the request was successfully sent to the server.
|
|
||||||
*/
|
|
||||||
public abstract void onRestoreTransactionsResponse(RestoreTransactions request,
|
|
||||||
ResponseCode responseCode);
|
|
||||||
|
|
||||||
private void initCompatibilityLayer() {
|
|
||||||
try {
|
|
||||||
mStartIntentSender = mActivity.getClass().getMethod("startIntentSender",
|
|
||||||
START_INTENT_SENDER_SIG);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
mStartIntentSender = null;
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
mStartIntentSender = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startBuyPageActivity(PendingIntent pendingIntent, Intent intent) {
|
|
||||||
if (mStartIntentSender != null) {
|
|
||||||
// This is on Android 2.0 and beyond. The in-app buy page activity
|
|
||||||
// must be on the activity stack of the application.
|
|
||||||
try {
|
|
||||||
// This implements the method call:
|
|
||||||
// mActivity.startIntentSender(pendingIntent.getIntentSender(),
|
|
||||||
// intent, 0, 0, 0);
|
|
||||||
mStartIntentSenderArgs[0] = pendingIntent.getIntentSender();
|
|
||||||
mStartIntentSenderArgs[1] = intent;
|
|
||||||
mStartIntentSenderArgs[2] = Integer.valueOf(0);
|
|
||||||
mStartIntentSenderArgs[3] = Integer.valueOf(0);
|
|
||||||
mStartIntentSenderArgs[4] = Integer.valueOf(0);
|
|
||||||
mStartIntentSender.invoke(mActivity, mStartIntentSenderArgs);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "error starting activity", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This is on Android version 1.6. The in-app buy page activity must be on its
|
|
||||||
// own separate activity stack instead of on the activity stack of
|
|
||||||
// the application.
|
|
||||||
try {
|
|
||||||
pendingIntent.send(mActivity, 0 /* code */, intent);
|
|
||||||
} catch (CanceledException e) {
|
|
||||||
Log.e(TAG, "error starting activity", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the UI after the database has been updated. This method runs
|
|
||||||
* in a background thread so it has to post a Runnable to run on the UI
|
|
||||||
* thread.
|
|
||||||
* @param purchaseState the purchase state of the item
|
|
||||||
* @param itemId a string identifying the item
|
|
||||||
* @param quantity the quantity of items in this purchase
|
|
||||||
*/
|
|
||||||
void postPurchaseStateChange(final PurchaseState purchaseState, final String itemId,
|
|
||||||
final int quantity, final long purchaseTime, final String developerPayload) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
onPurchaseStateChange(
|
|
||||||
purchaseState, itemId, quantity, purchaseTime, developerPayload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
|
||||||
|
|
||||||
package org.renpy.android.billing;
|
|
||||||
|
|
||||||
import org.renpy.android.Configuration;
|
|
||||||
import org.renpy.android.billing.BillingService.RequestPurchase;
|
|
||||||
import org.renpy.android.billing.BillingService.RestoreTransactions;
|
|
||||||
import org.renpy.android.billing.Consts.PurchaseState;
|
|
||||||
import org.renpy.android.billing.Consts.ResponseCode;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class contains the methods that handle responses from Android Market. The
|
|
||||||
* implementation of these methods is specific to a particular application.
|
|
||||||
* The methods in this example update the database and, if the main application
|
|
||||||
* has registered a {@llink PurchaseObserver}, will also update the UI. An
|
|
||||||
* application might also want to forward some responses on to its own server,
|
|
||||||
* and that could be done here (in a background thread) but this example does
|
|
||||||
* not do that.
|
|
||||||
*
|
|
||||||
* You should modify and obfuscate this code before using it.
|
|
||||||
*/
|
|
||||||
public class ResponseHandler {
|
|
||||||
private static final String TAG = "ResponseHandler";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a static instance of {@link PurchaseObserver} that the
|
|
||||||
* application creates and registers with this class. The PurchaseObserver
|
|
||||||
* is used for updating the UI if the UI is visible.
|
|
||||||
*/
|
|
||||||
private static PurchaseObserver sPurchaseObserver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an observer that updates the UI.
|
|
||||||
* @param observer the observer to register
|
|
||||||
*/
|
|
||||||
public static synchronized void register(PurchaseObserver observer) {
|
|
||||||
Log.d(TAG, "@@@@@@@@@22 RESPONSE HANDLER CALLED REGISTER()");
|
|
||||||
sPurchaseObserver = observer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters a previously registered observer.
|
|
||||||
* @param observer the previously registered observer.
|
|
||||||
*/
|
|
||||||
public static synchronized void unregister(PurchaseObserver observer) {
|
|
||||||
Log.d(TAG, "@@@@@@@@@22 RESPONSE HANDLER CALLED UNREGISTER()");
|
|
||||||
sPurchaseObserver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the application of the availability of the MarketBillingService.
|
|
||||||
* This method is called in response to the application calling
|
|
||||||
* {@link BillingService#checkBillingSupported()}.
|
|
||||||
* @param supported true if in-app billing is supported.
|
|
||||||
*/
|
|
||||||
public static void checkBillingSupportedResponse(boolean supported, String type) {
|
|
||||||
Log.d(TAG, "checkBillingSupportedResponse()");
|
|
||||||
if (sPurchaseObserver != null) {
|
|
||||||
sPurchaseObserver.onBillingSupported(supported, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a new activity for the user to buy an item for sale. This method
|
|
||||||
* forwards the intent on to the PurchaseObserver (if it exists) because
|
|
||||||
* we need to start the activity on the activity stack of the application.
|
|
||||||
*
|
|
||||||
* @param pendingIntent a PendingIntent that we received from Android Market that
|
|
||||||
* will create the new buy page activity
|
|
||||||
* @param intent an intent containing a request id in an extra field that
|
|
||||||
* will be passed to the buy page activity when it is created
|
|
||||||
*/
|
|
||||||
public static void buyPageIntentResponse(PendingIntent pendingIntent, Intent intent) {
|
|
||||||
Log.d(TAG, "buyPageIntentResponse()");
|
|
||||||
if (sPurchaseObserver == null) {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.d(TAG, "UI is not running");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sPurchaseObserver.startBuyPageActivity(pendingIntent, intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the application of purchase state changes. The application
|
|
||||||
* can offer an item for sale to the user via
|
|
||||||
* {@link BillingService#requestPurchase(String)}. The BillingService
|
|
||||||
* calls this method after it gets the response. Another way this method
|
|
||||||
* can be called is if the user bought something on another device running
|
|
||||||
* this same app. Then Android Market notifies the other devices that
|
|
||||||
* the user has purchased an item, in which case the BillingService will
|
|
||||||
* also call this method. Finally, this method can be called if the item
|
|
||||||
* was refunded.
|
|
||||||
* @param purchaseState the state of the purchase request (PURCHASED,
|
|
||||||
* CANCELED, or REFUNDED)
|
|
||||||
* @param productId a string identifying a product for sale
|
|
||||||
* @param orderId a string identifying the order
|
|
||||||
* @param purchaseTime the time the product was purchased, in milliseconds
|
|
||||||
* since the epoch (Jan 1, 1970)
|
|
||||||
* @param developerPayload the developer provided "payload" associated with
|
|
||||||
* the order
|
|
||||||
*/
|
|
||||||
public static void purchaseResponse(
|
|
||||||
final Context context, final PurchaseState purchaseState, final String productId,
|
|
||||||
final String orderId, final long purchaseTime, final String developerPayload,
|
|
||||||
final String purchaseToken, final String packageName) {
|
|
||||||
|
|
||||||
// Update the database with the purchase state. We shouldn't do that
|
|
||||||
// from the main thread so we do the work in a background thread.
|
|
||||||
// We don't update the UI here. We will update the UI after we update
|
|
||||||
// the database because we need to read and update the current quantity
|
|
||||||
// first.
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
PurchaseDatabase db = new PurchaseDatabase(context);
|
|
||||||
int quantity = db.updatePurchase(
|
|
||||||
Security.obfuscate(context, Configuration.billing_salt, orderId),
|
|
||||||
Security.obfuscate(context, Configuration.billing_salt, productId),
|
|
||||||
purchaseState,
|
|
||||||
purchaseTime,
|
|
||||||
Security.obfuscate(context, Configuration.billing_salt, developerPayload));
|
|
||||||
db.close();
|
|
||||||
|
|
||||||
// This needs to be synchronized because the UI thread can change the
|
|
||||||
// value of sPurchaseObserver.
|
|
||||||
synchronized(ResponseHandler.class) {
|
|
||||||
if (sPurchaseObserver != null) {
|
|
||||||
sPurchaseObserver.postPurchaseStateChange(
|
|
||||||
purchaseState, productId, quantity, purchaseTime, developerPayload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when we receive a response code from Android Market for a
|
|
||||||
* RequestPurchase request that we made. This is used for reporting various
|
|
||||||
* errors and also for acknowledging that an order was sent successfully to
|
|
||||||
* the server. This is NOT used for any purchase state changes. All
|
|
||||||
* purchase state changes are received in the {@link BillingReceiver} and
|
|
||||||
* are handled in {@link Security#verifyPurchase(String, String)}.
|
|
||||||
* @param context the context
|
|
||||||
* @param request the RequestPurchase request for which we received a
|
|
||||||
* response code
|
|
||||||
* @param responseCode a response code from Market to indicate the state
|
|
||||||
* of the request
|
|
||||||
*/
|
|
||||||
public static void responseCodeReceived(Context context, RequestPurchase request,
|
|
||||||
ResponseCode responseCode) {
|
|
||||||
if (sPurchaseObserver != null) {
|
|
||||||
sPurchaseObserver.onRequestPurchaseResponse(request, responseCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called when we receive a response code from Android Market for a
|
|
||||||
* RestoreTransactions request.
|
|
||||||
* @param context the context
|
|
||||||
* @param request the RestoreTransactions request for which we received a
|
|
||||||
* response code
|
|
||||||
* @param responseCode a response code from Market to indicate the state
|
|
||||||
* of the request
|
|
||||||
*/
|
|
||||||
public static void responseCodeReceived(Context context, RestoreTransactions request,
|
|
||||||
ResponseCode responseCode) {
|
|
||||||
if (sPurchaseObserver != null) {
|
|
||||||
sPurchaseObserver.onRestoreTransactionsResponse(request, responseCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,286 +0,0 @@
|
||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
|
||||||
|
|
||||||
package org.renpy.android.billing;
|
|
||||||
|
|
||||||
import org.renpy.android.billing.Consts.PurchaseState;
|
|
||||||
import org.renpy.android.billing.util.Base64;
|
|
||||||
import org.renpy.android.billing.util.Base64DecoderException;
|
|
||||||
import org.renpy.android.PythonActivity;
|
|
||||||
import org.renpy.android.Configuration;
|
|
||||||
import org.renpy.android.billing.util.Installation;
|
|
||||||
import org.renpy.android.billing.util.AESObfuscator;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.provider.Settings;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security-related methods. For a secure implementation, all of this code
|
|
||||||
* should be implemented on a server that communicates with the
|
|
||||||
* application on the device. For the sake of simplicity and clarity of this
|
|
||||||
* example, this code is included here and is executed on the device. If you
|
|
||||||
* must verify the purchases on the phone, you should obfuscate this code to
|
|
||||||
* make it harder for an attacker to replace the code with stubs that treat all
|
|
||||||
* purchases as verified.
|
|
||||||
*/
|
|
||||||
public class Security {
|
|
||||||
private static final String TAG = "Security";
|
|
||||||
|
|
||||||
private static final String KEY_FACTORY_ALGORITHM = "RSA";
|
|
||||||
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
|
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This keeps track of the nonces that we generated and sent to the
|
|
||||||
* server. We need to keep track of these until we get back the purchase
|
|
||||||
* state and send a confirmation message back to Android Market. If we are
|
|
||||||
* killed and lose this list of nonces, it is not fatal. Android Market will
|
|
||||||
* send us a new "notify" message and we will re-generate a new nonce.
|
|
||||||
* This has to be "static" so that the {@link BillingReceiver} can
|
|
||||||
* check if a nonce exists.
|
|
||||||
*/
|
|
||||||
private static HashSet<Long> sKnownNonces = new HashSet<Long>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class to hold the verified purchase information.
|
|
||||||
*/
|
|
||||||
public static class VerifiedPurchase {
|
|
||||||
public PurchaseState purchaseState;
|
|
||||||
public String notificationId;
|
|
||||||
public String productId;
|
|
||||||
public String orderId;
|
|
||||||
public long purchaseTime;
|
|
||||||
public String developerPayload;
|
|
||||||
public String purchaseToken;
|
|
||||||
public String packageName;
|
|
||||||
|
|
||||||
public VerifiedPurchase(PurchaseState purchaseState, String notificationId,
|
|
||||||
String productId, String orderId, long purchaseTime, String developerPayload,
|
|
||||||
String purchaseToken, String packageName) {
|
|
||||||
this.purchaseState = purchaseState;
|
|
||||||
this.notificationId = notificationId;
|
|
||||||
this.productId = productId;
|
|
||||||
this.orderId = orderId;
|
|
||||||
this.purchaseTime = purchaseTime;
|
|
||||||
this.developerPayload = developerPayload;
|
|
||||||
this.purchaseToken = purchaseToken;
|
|
||||||
this.packageName = packageName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generates a nonce (a random number used once). */
|
|
||||||
public static long generateNonce() {
|
|
||||||
long nonce = RANDOM.nextLong();
|
|
||||||
sKnownNonces.add(nonce);
|
|
||||||
return nonce;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void removeNonce(long nonce) {
|
|
||||||
sKnownNonces.remove(nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isNonceKnown(long nonce) {
|
|
||||||
return sKnownNonces.contains(nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the data was signed with the given signature, and returns
|
|
||||||
* the list of verified purchases. The data is in JSON format and contains
|
|
||||||
* a nonce (number used once) that we generated and that was signed
|
|
||||||
* (as part of the whole data string) with a private key. The data also
|
|
||||||
* contains the {@link PurchaseState} and product ID of the purchase.
|
|
||||||
* In the general case, there can be an array of purchase transactions
|
|
||||||
* because there may be delays in processing the purchase on the backend
|
|
||||||
* and then several purchases can be batched together.
|
|
||||||
* @param signedData the signed JSON string (signed, not encrypted)
|
|
||||||
* @param signature the signature for the data, signed with the private key
|
|
||||||
*/
|
|
||||||
public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature) {
|
|
||||||
if (signedData == null) {
|
|
||||||
Log.e(TAG, "data is null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "signedData: " + signedData);
|
|
||||||
}
|
|
||||||
boolean verified = false;
|
|
||||||
if (!TextUtils.isEmpty(signature)) {
|
|
||||||
/**
|
|
||||||
* Compute your public key (that you got from the Android Market publisher site).
|
|
||||||
*
|
|
||||||
* Instead of just storing the entire literal string here embedded in the
|
|
||||||
* program, construct the key at runtime from pieces or
|
|
||||||
* use bit manipulation (for example, XOR with some other string) to hide
|
|
||||||
* the actual key. The key itself is not secret information, but we don't
|
|
||||||
* want to make it easy for an adversary to replace the public key with one
|
|
||||||
* of their own and then fake messages from the server.
|
|
||||||
*
|
|
||||||
* Generally, encryption keys / passwords should only be kept in memory
|
|
||||||
* long enough to perform the operation they need to perform.
|
|
||||||
*/
|
|
||||||
String base64EncodedPublicKey = Configuration.billing_pubkey;
|
|
||||||
PublicKey key = Security.generatePublicKey(base64EncodedPublicKey);
|
|
||||||
verified = Security.verify(key, signedData, signature);
|
|
||||||
if (!verified) {
|
|
||||||
Log.w(TAG, "signature does not match data.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject jObject;
|
|
||||||
JSONArray jTransactionsArray = null;
|
|
||||||
int numTransactions = 0;
|
|
||||||
long nonce = 0L;
|
|
||||||
try {
|
|
||||||
jObject = new JSONObject(signedData);
|
|
||||||
|
|
||||||
// The nonce might be null if the user backed out of the buy page.
|
|
||||||
nonce = jObject.optLong("nonce");
|
|
||||||
jTransactionsArray = jObject.optJSONArray("orders");
|
|
||||||
if (jTransactionsArray != null) {
|
|
||||||
numTransactions = jTransactionsArray.length();
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Security.isNonceKnown(nonce)) {
|
|
||||||
Log.w(TAG, "Nonce not found: " + nonce);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<VerifiedPurchase> purchases = new ArrayList<VerifiedPurchase>();
|
|
||||||
try {
|
|
||||||
for (int i = 0; i < numTransactions; i++) {
|
|
||||||
JSONObject jElement = jTransactionsArray.getJSONObject(i);
|
|
||||||
int response = jElement.getInt("purchaseState");
|
|
||||||
PurchaseState purchaseState = PurchaseState.valueOf(response);
|
|
||||||
String productId = jElement.getString("productId");
|
|
||||||
String packageName = jElement.getString("packageName");
|
|
||||||
long purchaseTime = jElement.getLong("purchaseTime");
|
|
||||||
String orderId = jElement.optString("orderId", "");
|
|
||||||
String notifyId = null;
|
|
||||||
if (jElement.has("notificationId")) {
|
|
||||||
notifyId = jElement.getString("notificationId");
|
|
||||||
}
|
|
||||||
String developerPayload = jElement.optString("developerPayload", null);
|
|
||||||
String purchaseToken = jElement.optString("purchaseToken", null);
|
|
||||||
|
|
||||||
// If the purchase state is PURCHASED, then we require a
|
|
||||||
// verified nonce.
|
|
||||||
if (purchaseState == PurchaseState.PURCHASED && !verified) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
purchases.add(new VerifiedPurchase(purchaseState, notifyId, productId,
|
|
||||||
orderId, purchaseTime, developerPayload, purchaseToken, packageName));
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(TAG, "JSON exception: ", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
removeNonce(nonce);
|
|
||||||
return purchases;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a PublicKey instance from a string containing the
|
|
||||||
* Base64-encoded public key.
|
|
||||||
*
|
|
||||||
* @param encodedPublicKey Base64-encoded public key
|
|
||||||
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
|
||||||
*/
|
|
||||||
public static PublicKey generatePublicKey(String encodedPublicKey) {
|
|
||||||
try {
|
|
||||||
byte[] decodedKey = Base64.decode(encodedPublicKey);
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
|
||||||
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
Log.e(TAG, "Invalid key specification.");
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
Log.e(TAG, "Base64 decoding failed.");
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the signature from the server matches the computed
|
|
||||||
* signature on the data. Returns true if the data is correctly signed.
|
|
||||||
*
|
|
||||||
* @param publicKey public key associated with the developer account
|
|
||||||
* @param signedData signed data from server
|
|
||||||
* @param signature server signature
|
|
||||||
* @return true if the data and signature match
|
|
||||||
*/
|
|
||||||
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
|
|
||||||
if (Consts.DEBUG) {
|
|
||||||
Log.i(TAG, "signature: " + signature);
|
|
||||||
}
|
|
||||||
Signature sig;
|
|
||||||
try {
|
|
||||||
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
|
|
||||||
sig.initVerify(publicKey);
|
|
||||||
sig.update(signedData.getBytes());
|
|
||||||
if (!sig.verify(Base64.decode(signature))) {
|
|
||||||
Log.e(TAG, "Signature verification failed.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Log.e(TAG, "NoSuchAlgorithmException.");
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
Log.e(TAG, "Invalid key specification.");
|
|
||||||
} catch (SignatureException e) {
|
|
||||||
Log.e(TAG, "Signature exception.");
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
Log.e(TAG, "Base64 decoding failed.");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AESObfuscator _obfuscator = null;
|
|
||||||
|
|
||||||
private static AESObfuscator getObfuscator(Context context, byte[] salt) {
|
|
||||||
if (_obfuscator == null) {
|
|
||||||
final String installationId = Installation.id(context);
|
|
||||||
final String deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
|
||||||
final String password = installationId + deviceId + context.getPackageName();
|
|
||||||
_obfuscator = new AESObfuscator(salt, password);
|
|
||||||
}
|
|
||||||
return _obfuscator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String unobfuscate(Context context, byte[] salt, String obfuscated) {
|
|
||||||
final AESObfuscator obfuscator = getObfuscator(context, salt);
|
|
||||||
try {
|
|
||||||
return obfuscator.unobfuscate(obfuscated);
|
|
||||||
} catch (AESObfuscator.ValidationException e) {
|
|
||||||
Log.w(TAG, "Invalid obfuscated data or key");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String obfuscate(Context context, byte[] salt, String original) {
|
|
||||||
final AESObfuscator obfuscator = getObfuscator(context, salt);
|
|
||||||
return obfuscator.obfuscate(original);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android.billing.util;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.spec.KeySpec;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import javax.crypto.SecretKeyFactory;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An obfuscator that uses AES to encrypt data.
|
|
||||||
*/
|
|
||||||
public class AESObfuscator {
|
|
||||||
private static final String UTF8 = "UTF-8";
|
|
||||||
private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
|
|
||||||
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
|
|
||||||
private static final byte[] IV =
|
|
||||||
{ 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
|
|
||||||
private static final String header = "org.android.billing.util.AESObfuscator-1|";
|
|
||||||
|
|
||||||
private Cipher mEncryptor;
|
|
||||||
private Cipher mDecryptor;
|
|
||||||
|
|
||||||
public AESObfuscator(byte[] salt, String password) {
|
|
||||||
try {
|
|
||||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
|
|
||||||
KeySpec keySpec =
|
|
||||||
new PBEKeySpec(password.toCharArray(), salt, 1024, 256);
|
|
||||||
SecretKey tmp = factory.generateSecret(keySpec);
|
|
||||||
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
|
|
||||||
mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
|
||||||
mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
|
|
||||||
mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
|
||||||
mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
// This can't happen on a compatible Android device.
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String obfuscate(String original) {
|
|
||||||
if (original == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Header is appended as an integrity check
|
|
||||||
return Base64.encode(mEncryptor.doFinal((header + original).getBytes(UTF8)));
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String unobfuscate(String obfuscated) throws ValidationException {
|
|
||||||
if (obfuscated == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
|
|
||||||
// Check for presence of header. This serves as a final integrity check, for cases
|
|
||||||
// where the block size is correct during decryption.
|
|
||||||
int headerIndex = result.indexOf(header);
|
|
||||||
if (headerIndex != 0) {
|
|
||||||
throw new ValidationException("Header not found (invalid data or key)" + ":" +
|
|
||||||
obfuscated);
|
|
||||||
}
|
|
||||||
return result.substring(header.length(), result.length());
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
|
||||||
} catch (IllegalBlockSizeException e) {
|
|
||||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
|
||||||
} catch (BadPaddingException e) {
|
|
||||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ValidationException extends Exception {
|
|
||||||
public ValidationException() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValidationException(String s) {
|
|
||||||
super(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,570 +0,0 @@
|
||||||
// Portions copyright 2002, Google, Inc.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package org.renpy.android.billing.util;
|
|
||||||
|
|
||||||
// This code was converted from code at http://iharder.sourceforge.net/base64/
|
|
||||||
// Lots of extraneous features were removed.
|
|
||||||
/* The original code said:
|
|
||||||
* <p>
|
|
||||||
* I am placing this code in the Public Domain. Do with it as you will.
|
|
||||||
* This software comes with no guarantees or warranties but with
|
|
||||||
* plenty of well-wishing instead!
|
|
||||||
* Please visit
|
|
||||||
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
|
||||||
* periodically to check for updates or to contribute improvements.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @author Robert Harder
|
|
||||||
* @author rharder@usa.net
|
|
||||||
* @version 1.3
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base64 converter class. This code is not a complete MIME encoder;
|
|
||||||
* it simply converts binary data to base64 data and back.
|
|
||||||
*
|
|
||||||
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
|
||||||
* class.
|
|
||||||
*/
|
|
||||||
public class Base64 {
|
|
||||||
/** Specify encoding (value is {@code true}). */
|
|
||||||
public final static boolean ENCODE = true;
|
|
||||||
|
|
||||||
/** Specify decoding (value is {@code false}). */
|
|
||||||
public final static boolean DECODE = false;
|
|
||||||
|
|
||||||
/** The equals sign (=) as a byte. */
|
|
||||||
private final static byte EQUALS_SIGN = (byte) '=';
|
|
||||||
|
|
||||||
/** The new line character (\n) as a byte. */
|
|
||||||
private final static byte NEW_LINE = (byte) '\n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 64 valid Base64 values.
|
|
||||||
*/
|
|
||||||
private final static byte[] ALPHABET =
|
|
||||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
|
||||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
|
||||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
|
||||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
|
||||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
|
||||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
|
||||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
|
||||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
|
||||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
|
||||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
|
||||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
|
||||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
|
||||||
(byte) '9', (byte) '+', (byte) '/'};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 64 valid web safe Base64 values.
|
|
||||||
*/
|
|
||||||
private final static byte[] WEBSAFE_ALPHABET =
|
|
||||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
|
||||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
|
||||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
|
||||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
|
||||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
|
||||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
|
||||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
|
||||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
|
||||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
|
||||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
|
||||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
|
||||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
|
||||||
(byte) '9', (byte) '-', (byte) '_'};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates a Base64 value to either its 6-bit reconstruction value
|
|
||||||
* or a negative number indicating some other meaning.
|
|
||||||
**/
|
|
||||||
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
|
||||||
-5, -5, // Whitespace: Tab and Linefeed
|
|
||||||
-9, -9, // Decimal 11 - 12
|
|
||||||
-5, // Whitespace: Carriage Return
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
|
||||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
|
||||||
-5, // Whitespace: Space
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
|
||||||
62, // Plus sign at decimal 43
|
|
||||||
-9, -9, -9, // Decimal 44 - 46
|
|
||||||
63, // Slash at decimal 47
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
|
||||||
-9, -9, -9, // Decimal 58 - 60
|
|
||||||
-1, // Equals sign at decimal 61
|
|
||||||
-9, -9, -9, // Decimal 62 - 64
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
|
||||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
|
||||||
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
|
||||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
|
||||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
|
||||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
|
||||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/** The web safe decodabet */
|
|
||||||
private final static byte[] WEBSAFE_DECODABET =
|
|
||||||
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
|
||||||
-5, -5, // Whitespace: Tab and Linefeed
|
|
||||||
-9, -9, // Decimal 11 - 12
|
|
||||||
-5, // Whitespace: Carriage Return
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
|
||||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
|
||||||
-5, // Whitespace: Space
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
|
||||||
62, // Dash '-' sign at decimal 45
|
|
||||||
-9, -9, // Decimal 46-47
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
|
||||||
-9, -9, -9, // Decimal 58 - 60
|
|
||||||
-1, // Equals sign at decimal 61
|
|
||||||
-9, -9, -9, // Decimal 62 - 64
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
|
||||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
|
||||||
-9, -9, -9, -9, // Decimal 91-94
|
|
||||||
63, // Underscore '_' at decimal 95
|
|
||||||
-9, // Decimal 96
|
|
||||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
|
||||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
|
||||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
|
||||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
|
||||||
};
|
|
||||||
|
|
||||||
// Indicates white space in encoding
|
|
||||||
private final static byte WHITE_SPACE_ENC = -5;
|
|
||||||
// Indicates equals sign in encoding
|
|
||||||
private final static byte EQUALS_SIGN_ENC = -1;
|
|
||||||
|
|
||||||
/** Defeats instantiation. */
|
|
||||||
private Base64() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ******** E N C O D I N G M E T H O D S ******** */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes up to three bytes of the array <var>source</var>
|
|
||||||
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
|
||||||
* The source and destination arrays can be manipulated
|
|
||||||
* anywhere along their length by specifying
|
|
||||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
|
||||||
* This method does not check to make sure your arrays
|
|
||||||
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
|
||||||
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
|
||||||
* the <var>destination</var> array.
|
|
||||||
* The actual number of significant bytes in your array is
|
|
||||||
* given by <var>numSigBytes</var>.
|
|
||||||
*
|
|
||||||
* @param source the array to convert
|
|
||||||
* @param srcOffset the index where conversion begins
|
|
||||||
* @param numSigBytes the number of significant bytes in your array
|
|
||||||
* @param destination the array to hold the conversion
|
|
||||||
* @param destOffset the index where output will be put
|
|
||||||
* @param alphabet is the encoding alphabet
|
|
||||||
* @return the <var>destination</var> array
|
|
||||||
* @since 1.3
|
|
||||||
*/
|
|
||||||
private static byte[] encode3to4(byte[] source, int srcOffset,
|
|
||||||
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
|
||||||
// 1 2 3
|
|
||||||
// 01234567890123456789012345678901 Bit position
|
|
||||||
// --------000000001111111122222222 Array position from threeBytes
|
|
||||||
// --------| || || || | Six bit groups to index alphabet
|
|
||||||
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
|
||||||
// 0x3f 0x3f 0x3f Additional AND
|
|
||||||
|
|
||||||
// Create buffer with zero-padding if there are only one or two
|
|
||||||
// significant bytes passed in the array.
|
|
||||||
// We have to shift left 24 in order to flush out the 1's that appear
|
|
||||||
// when Java treats a value as negative that is cast from a byte to an int.
|
|
||||||
int inBuff =
|
|
||||||
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
|
|
||||||
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
|
|
||||||
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
|
||||||
|
|
||||||
switch (numSigBytes) {
|
|
||||||
case 3:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
|
|
||||||
return destination;
|
|
||||||
case 2:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
destination[destOffset + 3] = EQUALS_SIGN;
|
|
||||||
return destination;
|
|
||||||
case 1:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = EQUALS_SIGN;
|
|
||||||
destination[destOffset + 3] = EQUALS_SIGN;
|
|
||||||
return destination;
|
|
||||||
default:
|
|
||||||
return destination;
|
|
||||||
} // end switch
|
|
||||||
} // end encode3to4
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
* Equivalent to calling
|
|
||||||
* {@code encodeBytes(source, 0, source.length)}
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static String encode(byte[] source) {
|
|
||||||
return encode(source, 0, source.length, ALPHABET, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into web safe Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @param doPadding is {@code true} to pad result with '=' chars
|
|
||||||
* if it does not fall on 3 byte boundaries
|
|
||||||
*/
|
|
||||||
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
|
||||||
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source the data to convert
|
|
||||||
* @param off offset in array where conversion should begin
|
|
||||||
* @param len length of data to convert
|
|
||||||
* @param alphabet the encoding alphabet
|
|
||||||
* @param doPadding is {@code true} to pad result with '=' chars
|
|
||||||
* if it does not fall on 3 byte boundaries
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
|
||||||
boolean doPadding) {
|
|
||||||
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
|
||||||
int outLen = outBuff.length;
|
|
||||||
|
|
||||||
// If doPadding is false, set length to truncate '='
|
|
||||||
// padding characters
|
|
||||||
while (doPadding == false && outLen > 0) {
|
|
||||||
if (outBuff[outLen - 1] != '=') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
outLen -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new String(outBuff, 0, outLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source the data to convert
|
|
||||||
* @param off offset in array where conversion should begin
|
|
||||||
* @param len length of data to convert
|
|
||||||
* @param alphabet is the encoding alphabet
|
|
||||||
* @param maxLineLength maximum length of one line.
|
|
||||||
* @return the BASE64-encoded byte array
|
|
||||||
*/
|
|
||||||
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
|
||||||
int maxLineLength) {
|
|
||||||
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
|
||||||
int len43 = lenDiv3 * 4;
|
|
||||||
byte[] outBuff = new byte[len43 // Main 4:3
|
|
||||||
+ (len43 / maxLineLength)]; // New lines
|
|
||||||
|
|
||||||
int d = 0;
|
|
||||||
int e = 0;
|
|
||||||
int len2 = len - 2;
|
|
||||||
int lineLength = 0;
|
|
||||||
for (; d < len2; d += 3, e += 4) {
|
|
||||||
|
|
||||||
// The following block of code is the same as
|
|
||||||
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
|
||||||
// but inlined for faster encoding (~20% improvement)
|
|
||||||
int inBuff =
|
|
||||||
((source[d + off] << 24) >>> 8)
|
|
||||||
| ((source[d + 1 + off] << 24) >>> 16)
|
|
||||||
| ((source[d + 2 + off] << 24) >>> 24);
|
|
||||||
outBuff[e] = alphabet[(inBuff >>> 18)];
|
|
||||||
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
|
|
||||||
|
|
||||||
lineLength += 4;
|
|
||||||
if (lineLength == maxLineLength) {
|
|
||||||
outBuff[e + 4] = NEW_LINE;
|
|
||||||
e++;
|
|
||||||
lineLength = 0;
|
|
||||||
} // end if: end of line
|
|
||||||
} // end for: each piece of array
|
|
||||||
|
|
||||||
if (d < len) {
|
|
||||||
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
|
||||||
|
|
||||||
lineLength += 4;
|
|
||||||
if (lineLength == maxLineLength) {
|
|
||||||
// Add a last newline
|
|
||||||
outBuff[e + 4] = NEW_LINE;
|
|
||||||
e++;
|
|
||||||
}
|
|
||||||
e += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert (e == outBuff.length);
|
|
||||||
return outBuff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ******** D E C O D I N G M E T H O D S ******** */
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes four bytes from array <var>source</var>
|
|
||||||
* and writes the resulting bytes (up to three of them)
|
|
||||||
* to <var>destination</var>.
|
|
||||||
* The source and destination arrays can be manipulated
|
|
||||||
* anywhere along their length by specifying
|
|
||||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
|
||||||
* This method does not check to make sure your arrays
|
|
||||||
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
|
||||||
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
|
||||||
* the <var>destination</var> array.
|
|
||||||
* This method returns the actual number of bytes that
|
|
||||||
* were converted from the Base64 encoding.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param source the array to convert
|
|
||||||
* @param srcOffset the index where conversion begins
|
|
||||||
* @param destination the array to hold the conversion
|
|
||||||
* @param destOffset the index where output will be put
|
|
||||||
* @param decodabet the decodabet for decoding Base64 content
|
|
||||||
* @return the number of decoded bytes converted
|
|
||||||
* @since 1.3
|
|
||||||
*/
|
|
||||||
private static int decode4to3(byte[] source, int srcOffset,
|
|
||||||
byte[] destination, int destOffset, byte[] decodabet) {
|
|
||||||
// Example: Dk==
|
|
||||||
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
|
||||||
return 1;
|
|
||||||
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
|
||||||
// Example: DkL=
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
|
||||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
|
||||||
destination[destOffset + 1] = (byte) (outBuff >>> 8);
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
// Example: DkLE
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
|
||||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
|
|
||||||
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >> 16);
|
|
||||||
destination[destOffset + 1] = (byte) (outBuff >> 8);
|
|
||||||
destination[destOffset + 2] = (byte) (outBuff);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
} // end decodeToBytes
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes data from Base64 notation.
|
|
||||||
*
|
|
||||||
* @param s the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static byte[] decode(String s) throws Base64DecoderException {
|
|
||||||
byte[] bytes = s.getBytes();
|
|
||||||
return decode(bytes, 0, bytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes data from web safe Base64 notation.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param s the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
|
||||||
byte[] bytes = s.getBytes();
|
|
||||||
return decodeWebSafe(bytes, 0, bytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source The Base64 encoded data
|
|
||||||
* @return decoded data
|
|
||||||
* @since 1.3
|
|
||||||
* @throws Base64DecoderException
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
|
||||||
return decode(source, 0, source.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes web safe Base64 content in byte array format and returns
|
|
||||||
* the decoded data.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param source the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(byte[] source)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decodeWebSafe(source, 0, source.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source the Base64 encoded data
|
|
||||||
* @param off the offset of where to begin decoding
|
|
||||||
* @param len the length of characters to decode
|
|
||||||
* @return decoded data
|
|
||||||
* @since 1.3
|
|
||||||
* @throws Base64DecoderException
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source, int off, int len)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decode(source, off, len, DECODABET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes web safe Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param source the Base64 encoded data
|
|
||||||
* @param off the offset of where to begin decoding
|
|
||||||
* @param len the length of characters to decode
|
|
||||||
* @return decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decode(source, off, len, WEBSAFE_DECODABET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content using the supplied decodabet and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source the Base64 encoded data
|
|
||||||
* @param off the offset of where to begin decoding
|
|
||||||
* @param len the length of characters to decode
|
|
||||||
* @param decodabet the decodabet for decoding Base64 content
|
|
||||||
* @return decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
int len34 = len * 3 / 4;
|
|
||||||
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
|
||||||
int outBuffPosn = 0;
|
|
||||||
|
|
||||||
byte[] b4 = new byte[4];
|
|
||||||
int b4Posn = 0;
|
|
||||||
int i = 0;
|
|
||||||
byte sbiCrop = 0;
|
|
||||||
byte sbiDecode = 0;
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
|
|
||||||
sbiDecode = decodabet[sbiCrop];
|
|
||||||
|
|
||||||
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
|
||||||
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
|
||||||
// An equals sign (for padding) must not occur at position 0 or 1
|
|
||||||
// and must be the last byte[s] in the encoded value
|
|
||||||
if (sbiCrop == EQUALS_SIGN) {
|
|
||||||
int bytesLeft = len - i;
|
|
||||||
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
|
|
||||||
if (b4Posn == 0 || b4Posn == 1) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"invalid padding byte '=' at byte offset " + i);
|
|
||||||
} else if ((b4Posn == 3 && bytesLeft > 2)
|
|
||||||
|| (b4Posn == 4 && bytesLeft > 1)) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"padding byte '=' falsely signals end of encoded value "
|
|
||||||
+ "at offset " + i);
|
|
||||||
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"encoded value has invalid trailing byte");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
b4[b4Posn++] = sbiCrop;
|
|
||||||
if (b4Posn == 4) {
|
|
||||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
|
||||||
b4Posn = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Base64DecoderException("Bad Base64 input character at " + i
|
|
||||||
+ ": " + source[i + off] + "(decimal)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because web safe encoding allows non padding base64 encodes, we
|
|
||||||
// need to pad the rest of the b4 buffer with equal signs when
|
|
||||||
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
|
||||||
// four characters, so the b4 buffer must have two or three
|
|
||||||
// characters. This also catches the case where the input is
|
|
||||||
// padded with EQUALS_SIGN
|
|
||||||
if (b4Posn != 0) {
|
|
||||||
if (b4Posn == 1) {
|
|
||||||
throw new Base64DecoderException("single trailing character at offset "
|
|
||||||
+ (len - 1));
|
|
||||||
}
|
|
||||||
b4[b4Posn++] = EQUALS_SIGN;
|
|
||||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] out = new byte[outBuffPosn];
|
|
||||||
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
// Copyright 2002, Google, Inc.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package org.renpy.android.billing.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown when encountering an invalid Base64 input character.
|
|
||||||
*
|
|
||||||
* @author nelson
|
|
||||||
*/
|
|
||||||
public class Base64DecoderException extends Exception {
|
|
||||||
public Base64DecoderException() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Base64DecoderException(String s) {
|
|
||||||
super(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
/* Copyright 2011 Robot Media SL (http://www.robotmedia.net)
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.renpy.android.billing.util;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class Installation {
|
|
||||||
private static final String INSTALLATION = "INSTALLATION";
|
|
||||||
private static String sID = null;
|
|
||||||
|
|
||||||
public synchronized static String id(Context context) {
|
|
||||||
if (sID == null) {
|
|
||||||
File installation = new File(context.getFilesDir(), INSTALLATION);
|
|
||||||
try {
|
|
||||||
if (!installation.exists()) {
|
|
||||||
writeInstallationFile(installation);
|
|
||||||
}
|
|
||||||
sID = readInstallationFile(installation);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sID;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String readInstallationFile(File installation) throws IOException {
|
|
||||||
RandomAccessFile f = new RandomAccessFile(installation, "r");
|
|
||||||
byte[] bytes = new byte[(int) f.length()];
|
|
||||||
f.readFully(bytes);
|
|
||||||
f.close();
|
|
||||||
return new String(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeInstallationFile(File installation) throws IOException {
|
|
||||||
FileOutputStream out = new FileOutputStream(installation);
|
|
||||||
String id = UUID.randomUUID().toString();
|
|
||||||
out.write(id.getBytes());
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="{{args.package}}"
|
|
||||||
android:versionCode="{{args.numeric_version}}"
|
|
||||||
android:versionName="{{args.version}}"
|
|
||||||
android:installLocation="{{ args.install_location }}"
|
|
||||||
>
|
|
||||||
|
|
||||||
<supports-screens
|
|
||||||
android:smallScreens="true"
|
|
||||||
android:normalScreens="true"
|
|
||||||
android:largeScreens="true"
|
|
||||||
android:anyDensity="true"
|
|
||||||
{% if args.min_sdk_version >= 9 %}
|
|
||||||
android:xlargeScreens="true"
|
|
||||||
{% endif %}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<application android:label="@string/appName"
|
|
||||||
android:icon="@drawable/icon"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
>
|
|
||||||
|
|
||||||
{% for m in args.meta_data %}
|
|
||||||
<meta-data android:name="{{ m.split('=', 1)[0] }}" android:value="{{ m.split('=', 1)[-1] }}"/>{% endfor %}
|
|
||||||
<meta-data android:name="fullscreen" android:value="{% if args.window %}0{% else %}1{% endif %}"/>
|
|
||||||
<meta-data android:name="wakelock" android:value="{% if args.wakelock %}1{% else %}0{% endif %}"/>
|
|
||||||
|
|
||||||
|
|
||||||
<activity android:name="org.renpy.android.PythonActivity"
|
|
||||||
android:label="@string/iconName"
|
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|fontScale|uiMode{% if args.min_sdk_version >= 13 %}|screenSize{% endif %}"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:process=":python"
|
|
||||||
android:screenOrientation="{{ args.orientation }}"
|
|
||||||
android:windowSoftInputMode="stateAlwaysHidden"
|
|
||||||
>
|
|
||||||
|
|
||||||
{% if args.launcher %}
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.renpy.LAUNCH" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="{{ url_scheme }}" />
|
|
||||||
</intent-filter>
|
|
||||||
{% else %}
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%if args.ouya_category %}
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
<category android:name="tv.ouya.intent.category.{{ args.ouya_category }}"/>
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%if args.intent_filters %}
|
|
||||||
{{ intent_filters }}
|
|
||||||
{% endif %}
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
{% if args.launcher %}
|
|
||||||
<activity android:name="org.renpy.android.ProjectChooser"
|
|
||||||
android:label="@string/iconName">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if service or args.launcher %}
|
|
||||||
<service android:name="org.renpy.android.PythonService"
|
|
||||||
android:process=":PythonService"/>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if args.billing_pubkey %}
|
|
||||||
<service android:name="org.renpy.android.billing.BillingService"
|
|
||||||
android:process=":python" />
|
|
||||||
<receiver
|
|
||||||
android:name="org.renpy.android.billing.BillingReceiver"
|
|
||||||
android:process=":python">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
|
|
||||||
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
|
|
||||||
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="{{ args.min_sdk_version }}" android:targetSdkVersion="{{ args.sdk_version }}"/>
|
|
||||||
{% if args.wakelock %}
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
{% endif %}
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
|
|
||||||
{% for perm in args.permissions %}
|
|
||||||
{% if '.' in perm %}
|
|
||||||
<uses-permission android:name="{{ perm }}" />
|
|
||||||
{% else %}
|
|
||||||
<uses-permission android:name="android.permission.{{ perm }}" />
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if args.billing_pubkey %}
|
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{{ manifest_extra }}
|
|
||||||
</manifest>
|
|
|
@ -1,14 +0,0 @@
|
||||||
// Autogenerated file by build.py, don't change!
|
|
||||||
|
|
||||||
package org.renpy.android;
|
|
||||||
|
|
||||||
public class Configuration {
|
|
||||||
{% if args.billing_pubkey %}
|
|
||||||
static public boolean use_billing = true;
|
|
||||||
static public String billing_pubkey = "{{ args.billing_pubkey }}";
|
|
||||||
{% else %}
|
|
||||||
static public boolean use_billing = false;
|
|
||||||
static public String billing_pubkey = null;
|
|
||||||
{% endif %}
|
|
||||||
static public byte billing_salt[] = new byte[] {41, -90, -116, -41, 66, -53, 122, -110, -127, -96, -88, 77, 127, 115, 1, 73, 57, 110, 48, -116};
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project name="{{ versioned_name }}" default="help">
|
|
||||||
|
|
||||||
<loadproperties srcFile="local.properties" />
|
|
||||||
<property file="ant.properties" />
|
|
||||||
<loadproperties srcFile="project.properties" />
|
|
||||||
|
|
||||||
<property environment="env" />
|
|
||||||
<property file="build.properties" />
|
|
||||||
|
|
||||||
<fail
|
|
||||||
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project'"
|
|
||||||
unless="sdk.dir"
|
|
||||||
/>
|
|
||||||
<target name="-pre-build">
|
|
||||||
</target>
|
|
||||||
<target name="-pre-compile">
|
|
||||||
</target>
|
|
||||||
<target name="-post-compile">
|
|
||||||
</target>
|
|
||||||
<import file="${sdk.dir}/tools/ant/build.xml" />
|
|
||||||
|
|
||||||
</project>
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue