diff --git a/.gitignore b/.gitignore
index 1819886a78..abacb365ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,7 @@ r2/_builder.egg-info/
r2/_normalized_hot.egg-info/
r2/_sorts.egg-info/
r2/r2/lib/_normalized_hot.c
+r2/r2/lib/mr_tools/_mr_tools.c
r2/r2/lib/db/_sorts.c
r2/r2/lib/sgm.c
r2/r2/lib/utils/_utils.c
diff --git a/config/cassandra/cassandra.yaml b/config/cassandra/cassandra.yaml
new file mode 100644
index 0000000000..356c76e907
--- /dev/null
+++ b/config/cassandra/cassandra.yaml
@@ -0,0 +1,479 @@
+# Cassandra storage config YAML
+
+# NOTE:
+# See http://wiki.apache.org/cassandra/StorageConfiguration for
+# full explanations of configuration directives
+# /NOTE
+
+# The name of the cluster. This is mainly used to prevent machines in
+# one logical cluster from joining another.
+cluster_name: 'reddit'
+
+# You should always specify InitialToken when setting up a production
+# cluster for the first time, and often when adding capacity later.
+# The principle is that each node should be given an equal slice of
+# the token ring; see http://wiki.apache.org/cassandra/Operations
+# for more details.
+#
+# If blank, Cassandra will request a token bisecting the range of
+# the heaviest-loaded existing node. If there is no load information
+# available, such as is the case with a new cluster, it will pick
+# a random token, which will lead to hot spots.
+initial_token:
+
+# Set to true to make new [non-seed] nodes automatically migrate data
+# to themselves from the pre-existing nodes in the cluster. Defaults
+# to false because you can only bootstrap N machines at a time from
+# an existing cluster of N, so if you are bringing up a cluster of
+# 10 machines with 3 seeds you would have to do it in stages. Leaving
+# this off for the initial start simplifies that.
+auto_bootstrap: false
+
+# See http://wiki.apache.org/cassandra/HintedHandoff
+hinted_handoff_enabled: true
+
+# authentication backend, implementing IAuthenticator; used to identify users
+authenticator: org.apache.cassandra.auth.AllowAllAuthenticator
+
+# authorization backend, implementing IAuthority; used to limit access/provide permissions
+authority: org.apache.cassandra.auth.AllowAllAuthority
+
+# The partitioner is responsible for distributing rows (by key) across
+# nodes in the cluster. Any IPartitioner may be used, including your
+# own as long as it is on the classpath. Out of the box, Cassandra
+# provides org.apache.cassandra.dht.RandomPartitioner
+# org.apache.cassandra.dht.ByteOrderedPartitioner,
+# org.apache.cassandra.dht.OrderPreservingPartitioner (deprecated),
+# and org.apache.cassandra.dht.CollatingOrderPreservingPartitioner
+# (deprecated).
+#
+# - RandomPartitioner distributes rows across the cluster evenly by md5.
+# When in doubt, this is the best option.
+# - ByteOrderedPartitioner orders rows lexically by key bytes. BOP allows
+# scanning rows in key order, but the ordering can generate hot spots
+# for sequential insertion workloads.
+# - OrderPreservingPartitioner is an obsolete form of BOP, that stores
+# - keys in a less-efficient format and only works with keys that are
+# UTF8-encoded Strings.
+# - CollatingOPP colates according to EN,US rules rather than lexical byte
+# ordering. Use this as an example if you need custom collation.
+#
+# See http://wiki.apache.org/cassandra/Operations for more on
+# partitioners and token selection.
+partitioner: org.apache.cassandra.dht.RandomPartitioner
+
+# directories where Cassandra should store data on disk.
+data_file_directories:
+ - /cassandra/data
+
+# commit log
+commitlog_directory: /cassandra/commitlog
+
+# saved caches
+saved_caches_directory: /cassandra/saved_caches
+
+# Size to allow commitlog to grow to before creating a new segment
+commitlog_rotation_threshold_in_mb: 128
+
+# commitlog_sync may be either "periodic" or "batch."
+# When in batch mode, Cassandra won't ack writes until the commit log
+# has been fsynced to disk. It will wait up to
+# CommitLogSyncBatchWindowInMS milliseconds for other writes, before
+# performing the sync.
+commitlog_sync: periodic
+
+# the other option is "timed," where writes may be acked immediately
+# and the CommitLog is simply synced every commitlog_sync_period_in_ms
+# milliseconds.
+commitlog_sync_period_in_ms: 10000
+
+# Addresses of hosts that are deemed contact points.
+# Cassandra nodes use this list of hosts to find each other and learn
+# the topology of the ring. You must change this if you are running
+# multiple nodes!
+seeds:
+ - pmc01
+ - pmc02
+ - pmc03
+ - pmc04
+ - pmc05
+ - pmc06
+ - pmc07
+ - pmc08
+ - pmc09
+ - pmc10
+ - pmc11
+ - pmc12
+ - pmc13
+ - pmc14
+ - pmc15
+ - pmc16
+ - pmc17
+ - pmc18
+ - pmc19
+ - pmc20
+
+# Access mode. mmapped i/o is substantially faster, but only practical on
+# a 64bit machine (which notably does not include EC2 "small" instances)
+# or relatively small datasets. "auto", the safe choice, will enable
+# mmapping on a 64bit JVM. Other values are "mmap", "mmap_index_only"
+# (which may allow you to get part of the benefits of mmap on a 32bit
+# machine by mmapping only index files) and "standard".
+# (The buffer size settings that follow only apply to standard,
+# non-mmapped i/o.)
+disk_access_mode: mmap_index_only
+
+# Unlike most systems, in Cassandra writes are faster than reads, so
+# you can afford more of those in parallel. A good rule of thumb is 2
+# concurrent reads per processor core. Increase ConcurrentWrites to
+# the number of clients writing at once if you enable CommitLogSync +
+# CommitLogSyncDelay. -->
+concurrent_reads: 8
+concurrent_writes: 32
+
+# This sets the amount of memtable flush writer threads. These will
+# be blocked by disk io, and each one will hold a memtable in memory
+# while blocked. If you have a large heap and many data directories,
+# you can increase this value for better flush performance.
+# By default this will be set to the amount of data directories defined.
+#memtable_flush_writers: 1
+
+# Buffer size to use when performing contiguous column slices.
+# Increase this to the size of the column slices you typically perform
+sliced_buffer_size_in_kb: 64
+
+# TCP port, for commands and data
+storage_port: 7000
+
+# Address to bind to and tell other Cassandra nodes to connect to. You
+# _must_ change this if you want multiple nodes to be able to
+# communicate!
+#
+# Leaving it blank leaves it up to InetAddress.getLocalHost(). This
+# will always do the Right Thing *if* the node is properly configured
+# (hostname, name resolution, etc), and the Right Thing is to use the
+# address associated with the hostname (it might not be).
+#
+# Setting this to 0.0.0.0 is always wrong.
+listen_address:
+
+# The address to bind the Thrift RPC service to -- clients connect
+# here. Unlike ListenAddress above, you *can* specify 0.0.0.0 here if
+# you want Thrift to listen on all interfaces.
+#
+# Leaving this blank has the same effect it does for ListenAddress,
+# (i.e. it will be based on the configured hostname of the node).
+rpc_address: 0.0.0.0
+# port for Thrift to listen for clients on
+rpc_port: 9160
+
+# enable or disable keepalive on rpc connections
+rpc_keepalive: true
+
+# uncomment to set socket buffer sizes on rpc connections
+# rpc_send_buff_size_in_bytes:
+# rpc_recv_buff_size_in_bytes:
+
+# Frame size for thrift (maximum field length).
+# 0 disables TFramedTransport in favor of TSocket. This option
+# is deprecated; we strongly recommend using Framed mode.
+thrift_framed_transport_size_in_mb: 15
+
+# The max length of a thrift message, including all fields and
+# internal thrift overhead.
+thrift_max_message_length_in_mb: 16
+
+# Whether or not to take a snapshot before each compaction. Be
+# careful using this option, since Cassandra won't clean up the
+# snapshots for you. Mostly useful if you're paranoid when there
+# is a data format change.
+snapshot_before_compaction: false
+
+# change this to increase the compaction thread's priority. In java, 1 is the
+# lowest priority and that is our default.
+# compaction_thread_priority: 1
+
+# The threshold size in megabytes the binary memtable must grow to,
+# before it's submitted for flushing to disk.
+binary_memtable_throughput_in_mb: 256
+
+# Add column indexes to a row after its contents reach this size.
+# Increase if your column values are large, or if you have a very large
+# number of columns. The competing causes are, Cassandra has to
+# deserialize this much of the row to read a single column, so you want
+# it to be small - at least if you do many partial-row reads - but all
+# the index data is read for each access, so you don't want to generate
+# that wastefully either.
+column_index_size_in_kb: 64
+
+# Size limit for rows being compacted in memory. Larger rows will spill
+# over to disk and use a slower two-pass compaction process. A message
+# will be logged specifying the row key.
+in_memory_compaction_limit_in_mb: 64
+
+# Time to wait for a reply from other nodes before failing the command
+rpc_timeout_in_ms: 20000
+
+# phi value that must be reached for a host to be marked down.
+# most users should never need to adjust this.
+phi_convict_threshold: 10
+
+# endpoint_snitch -- Set this to a class that implements
+# IEndpointSnitch, which will let Cassandra know enough
+# about your network topology to route requests efficiently.
+# Out of the box, Cassandra provides
+# - org.apache.cassandra.locator.SimpleSnitch:
+# Treats Strategy order as proximity. This improves cache locality
+# when disabling read repair, which can further improve throughput.
+# - org.apache.cassandra.locator.RackInferringSnitch:
+# Proximity is determined by rack and data center, which are
+# assumed to correspond to the 3rd and 2nd octet of each node's
+# IP address, respectively
+# org.apache.cassandra.locator.PropertyFileSnitch:
+# - Proximity is determined by rack and data center, which are
+# explicitly configured in cassandra-topology.properties.
+endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
+
+# dynamic_snitch -- This boolean controls whether the above snitch is
+# wrapped with a dynamic snitch, which will monitor read latencies
+# and avoid reading from hosts that have slowed (due to compaction,
+# for instance)
+dynamic_snitch: true
+# controls how often to perform the more expensive part of host score
+# calculation
+dynamic_snitch_update_interval_in_ms: 100
+# controls how often to reset all host scores, allowing a bad host to
+# possibly recover
+dynamic_snitch_reset_interval_in_ms: 600000
+# if set greater than zero and read_repair_chance is < 1.0, this will allow
+# 'pinning' of replicas to hosts in order to increase cache capacity.
+# The badness threshold will control how much worse the pinned host has to be
+# before the dynamic snitch will prefer other replicas over it. This is
+# expressed as a double which represents a percentage. Thus, a value of
+# 0.2 means Cassandra would continue to prefer the static snitch values
+# until the pinned host was 20% worse than the fastest.
+dynamic_snitch_badness_threshold: 0.1
+
+# request_scheduler -- Set this to a class that implements
+# RequestScheduler, which will schedule incoming client requests
+# according to the specific policy. This is useful for multi-tenancy
+# with a single Cassandra cluster.
+# NOTE: This is specifically for requests from the client and does
+# not affect inter node communication.
+# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place
+# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of
+# client requests to a node with a separate queue for each
+# request_scheduler_id. The scheduler is further customized by
+# request_scheduler_options as described below.
+request_scheduler: org.apache.cassandra.scheduler.NoScheduler
+
+# Scheduler Options vary based on the type of scheduler
+# NoScheduler - Has no options
+# RoundRobin
+# - throttle_limit -- The throttle_limit is the number of in-flight
+# requests per client. Requests beyond
+# that limit are queued up until
+# running requests can complete.
+# The value of 80 here is twice the number of
+# concurrent_reads + concurrent_writes.
+# - default_weight -- default_weight is optional and allows for
+# overriding the default which is 1.
+# - weights -- Weights are optional and will default to 1 or the
+# overridden default_weight. The weight translates into how
+# many requests are handled during each turn of the
+# RoundRobin, based on the scheduler id.
+#
+# request_scheduler_options:
+# throttle_limit: 80
+# default_weight: 5
+# weights:
+# Keyspace1: 1
+# Keyspace2: 5
+
+# request_scheduler_id -- An identifer based on which to perform
+# the request scheduling. Currently the only valid option is keyspace.
+# request_scheduler_id: keyspace
+
+# The Index Interval determines how large the sampling of row keys
+# is for a given SSTable. The larger the sampling, the more effective
+# the index is at the cost of space.
+index_interval: 128
+
+# Keyspaces have ColumnFamilies. (Usually 1 KS per application.)
+# ColumnFamilies have Rows. (Dozens of CFs per KS.)
+# Rows contain Columns. (Many per CF.)
+# Columns contain name:value:timestamp. (Many per Row.)
+#
+# A KS is most similar to a schema, and a CF is most similar to a relational table.
+#
+# Keyspaces, ColumnFamilies, and Columns may carry additional
+# metadata that change their behavior. These are as follows:
+#
+# Keyspace required parameters:
+# - name: name of the keyspace; "system" is
+# reserved for Cassandra Internals.
+# - replica_placement_strategy: the class that determines how replicas
+# are distributed among nodes. Contains both the class as well as
+# configuration information. Must extend AbstractReplicationStrategy.
+# Out of the box, Cassandra provides
+# * org.apache.cassandra.locator.SimpleStrategy
+# * org.apache.cassandra.locator.NetworkTopologyStrategy
+# * org.apache.cassandra.locator.OldNetworkTopologyStrategy
+#
+# SimpleStrategy merely places the first
+# replica at the node whose token is closest to the key (as determined
+# by the Partitioner), and additional replicas on subsequent nodes
+# along the ring in increasing Token order.
+#
+# With NetworkTopologyStrategy,
+# for each datacenter, you can specify how many replicas you want
+# on a per-keyspace basis. Replicas are placed on different racks
+# within each DC, if possible. This strategy also requires rack aware
+# snitch, such as RackInferringSnitch or PropertyFileSnitch.
+# An example:
+# - name: Keyspace1
+# replica_placement_strategy: org.apache.cassandra.locator.NetworkTopologyStrategy
+# strategy_options:
+# DC1 : 3
+# DC2 : 2
+# DC3 : 1
+#
+# OldNetworkToplogyStrategy [formerly RackAwareStrategy]
+# places one replica in each of two datacenters, and the third on a
+# different rack in in the first. Additional datacenters are not
+# guaranteed to get a replica. Additional replicas after three are placed
+# in ring order after the third without regard to rack or datacenter.
+# - replication_factor: Number of replicas of each row
+# Keyspace optional paramaters:
+# - strategy_options: Additional information for the replication strategy.
+# - column_families:
+# ColumnFamily required parameters:
+# - name: name of the ColumnFamily. Must not contain the character "-".
+# - compare_with: tells Cassandra how to sort the columns for slicing
+# operations. The default is BytesType, which is a straightforward
+# lexical comparison of the bytes in each column. Other options are
+# AsciiType, UTF8Type, LexicalUUIDType, TimeUUIDType, LongType,
+# and IntegerType (a generic variable-length integer type).
+# You can also specify the fully-qualified class name to a class of
+# your choice extending org.apache.cassandra.db.marshal.AbstractType.
+#
+# ColumnFamily optional parameters:
+# - keys_cached: specifies the number of keys per sstable whose
+# locations we keep in memory in "mostly LRU" order. (JUST the key
+# locations, NOT any column values.) Specify a fraction (value less
+# than 1) or an absolute number of keys to cache. Defaults to 200000
+# keys.
+# - rows_cached: specifies the number of rows whose entire contents we
+# cache in memory. Do not use this on ColumnFamilies with large rows,
+# or ColumnFamilies with high write:read ratios. Specify a fraction
+# (value less than 1) or an absolute number of rows to cache.
+# Defaults to 0. (i.e. row caching is off by default)
+# - comment: used to attach additional human-readable information about
+# the column family to its definition.
+# - read_repair_chance: specifies the probability with which read
+# repairs should be invoked on non-quorum reads. must be between 0
+# and 1. defaults to 1.0 (always read repair).
+# - gc_grace_seconds: specifies the time to wait before garbage
+# collecting tombstones (deletion markers). defaults to 864000 (10
+# days). See http://wiki.apache.org/cassandra/DistributedDeletes
+# - default_validation_class: specifies a validator class to use for
+# validating all the column values in the CF.
+# NOTE:
+# min_ must be less than max_compaction_threshold!
+# - min_compaction_threshold: the minimum number of SSTables needed
+# to start a minor compaction. increasing this will cause minor
+# compactions to start less frequently and be more intensive. setting
+# this to 0 disables minor compactions. defaults to 4.
+# - max_compaction_threshold: the maximum number of SSTables allowed
+# before a minor compaction is forced. decreasing this will cause
+# minor compactions to start more frequently and be less intensive.
+# setting this to 0 disables minor compactions. defaults to 32.
+# /NOTE
+# - row_cache_save_period_in_seconds: number of seconds between saving
+# row caches. The row caches can be saved periodically and if one
+# exists on startup it will be loaded.
+# - key_cache_save_period_in_seconds: number of seconds between saving
+# key caches. The key caches can be saved periodically and if one
+# exists on startup it will be loaded.
+# - memtable_flush_after_mins: The maximum time to leave a dirty table
+# unflushed. This should be large enough that it won't cause a flush
+# storm of all memtables during periods of inactivity.
+# - memtable_throughput_in_mb: The maximum size of the memtable before
+# it is flushed. If undefined, 1/8 * heapsize will be used.
+# - memtable_operations_in_millions: Number of operations in millions
+# before the memtable is flushed. If undefined, throughput / 64 * 0.3
+# will be used.
+# - column_metadata:
+# Column required parameters:
+# - name: binds a validator (and optionally an indexer) to columns
+# with this name in any row of the enclosing column family.
+# - validator: like cf.compare_with, an AbstractType that checks
+# that the value of the column is well-defined.
+# Column optional parameters:
+# NOTE:
+# index_name cannot be set if index_type is not also set!
+# - index_name: User-friendly name for the index.
+# - index_type: The type of index to be created. Currently only
+# KEYS is supported.
+# /NOTE
+#
+# NOTE:
+# this keyspace definition is for demonstration purposes only.
+# Cassandra will not load these definitions during startup. See
+# http://wiki.apache.org/cassandra/FAQ#no_keyspaces for an explanation.
+# /NOTE
+keyspaces:
+ - name: reddit
+ replica_placement_strategy: org.apache.cassandra.locator.RackUnawareStrategy
+ replication_factor: 3
+ column_families:
+ - column_type: Standard
+ compare_with: BytesType
+ name: permacache
+ row_cache_save_period_in_seconds: 3600
+ rows_cached: 100000
+ - column_type: Standard
+ compare_with: BytesType
+ name: urls
+ row_cache_save_period_in_seconds: 3600
+ rows_cached: 100000
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: LinkVote
+ rows_cached: 100000
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: CommentVote
+ rows_cached: 100000
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: Friend
+ rows_cached: 10000
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: Save
+ rows_cached: 100000
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: Hide
+ rows_cached: 100000
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: Click
+ rows_cached: 100000
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: VotesByLink
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: VotesByDay
+ - column_type: Standard
+ name: FriendsByAccount
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: SavesByAccount
+ - column_type: Standard
+ compare_with: UTF8Type
+ name: CommentSortsCache
+ row_cache_save_period_in_seconds: 3600
+ rows_cached: 200000
diff --git a/config/cassandra/storage-conf.xml b/config/cassandra/storage-conf.xml
deleted file mode 100644
index a1189b815d..0000000000
--- a/config/cassandra/storage-conf.xml
+++ /dev/null
@@ -1,420 +0,0 @@
-
-
-
-
-
-
-
- reddit
-
-
- false
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
- org.apache.cassandra.locator.RackUnawareStrategy
-
-
- 1
-
-
- org.apache.cassandra.locator.EndPointSnitch
-
-
-
-
-
-
- org.apache.cassandra.locator.RackUnawareStrategy
- 3
- org.apache.cassandra.locator.EndPointSnitch
-
-
-
-
-
-
-
-
-
-
-
- org.apache.cassandra.locator.RackUnawareStrategy
- 3
- org.apache.cassandra.locator.EndPointSnitch
-
-
-
-
-
- org.apache.cassandra.auth.AllowAllAuthenticator
-
-
- org.apache.cassandra.dht.RandomPartitioner
-
-
-
-
-
- /cassandra/commitlog
-
- /cassandra/data
-
-
-
-
- pmc01
- pmc02
- pmc03
- pmc04
- pmc05
- pmc06
- pmc07
- pmc08
- pmc09
- pmc10
- pmc11
- pmc12
-
-
-
-
-
- 30000
-
- 10
-
- 128
-
-
-
-
-
-
-
- 7000
-
-
-
-
- 9160
-
- false
-
-
-
-
-
-
-
- mmap_index_only
-
-
- 512
-
-
- 64
-
-
- 32
- 8
-
-
- 64
-
-
- 128
-
- 256
-
- 0.3
-
- 60
-
-
- 8
- 32
-
-
- periodic
-
- 10000
-
-
-
-
- 864000
-
diff --git a/r2/example.ini b/r2/example.ini
index 342a847149..e80cab8552 100644
--- a/r2/example.ini
+++ b/r2/example.ini
@@ -32,9 +32,11 @@ MODSECRET = abcdefghijklmnopqrstuvwxyz0123456789
# secret for /prefs/feeds
FEEDSECRET = abcdefghijklmnopqrstuvwxyz0123456789
+INDEXTANK_API_URL =
+
# -- important settings --
# the domain that this app serves itself up as
-domain = localhost
+domain = reddit.local
# if you use www for the old-timey feel, put it here
domain_prefix =
# the user used for "system" operations and messages
@@ -80,6 +82,7 @@ admin_message_acct = reddit
# data cache (used for caching Thing objects)
num_mc_clients = 5
memcaches = 127.0.0.1:11211
+stalecaches =
# render caches (the second is "remote" and the local is optional but in the same format)
local_rendercache =
rendercaches = 127.0.0.1:11211
@@ -94,7 +97,7 @@ permacache_memcaches = 127.0.0.1:11211
cassandra_seeds = 127.0.0.1:9160
# read/write consistency levels for Cassandra
cassandra_rcl = ONE
-cassandra_wcl = QUORUM
+cassandra_wcl = ONE
# -- url cache options --
url_caches = 127.0.0.1:11211
@@ -117,6 +120,8 @@ adtracker_url = /static/pixel.png
adframetracker_url = http://pixel.reddit.com/pixel/of_defenestration.png
# open redirector to bounce clicks off of on sponsored links for tracking
clicktracker_url = /static/pixel.png
+# new pixel
+newtracker_url =
# amqp
amqp_host = localhost:5672
@@ -128,17 +133,23 @@ amqp_virtual_host = /
# list of all databases named in the subsequent table
databases = main, comment, vote, email, authorize, award, hc
-#db name db host user, pass
-main_db = reddit, 127.0.0.1, reddit, password
-comment_db = reddit, 127.0.0.1, reddit, password
-comment2_db = reddit, 127.0.0.1, reddit, password
-vote_db = reddit, 127.0.0.1, reddit, password
-email_db = reddit, 127.0.0.1, reddit, password
-authorize_db = reddit, 127.0.0.1, reddit, password
-award_db = reddit, 127.0.0.1, reddit, password
-hc_db = reddit, 127.0.0.1, reddit, password
+db_user = reddit
+db_pass = password
+db_port = 5432
+db_pool_size = 3
+db_pool_overflow_size = 3
+
+#db name db host user, pass, port, conn, overflow_conn
+main_db = reddit, 127.0.0.1, *, *, *, *, *
+comment_db = reddit, 127.0.0.1, *, *, *, *, *
+comment2_db = reddit, 127.0.0.1, *, *, *, *, *
+vote_db = reddit, 127.0.0.1, *, *, *, *, *
+email_db = reddit, 127.0.0.1, *, *, *, *, *
+authorize_db = reddit, 127.0.0.1, *, *, *, *, *
+award_db = reddit, 127.0.0.1, *, *, *, *, *
+hc_db = reddit, 127.0.0.1, *, *, *, *, *
-hardcache_categories = *:hc
+hardcache_categories = *:hc:hc
# this setting will prefix all of the table names
db_app_name = reddit
@@ -197,13 +208,17 @@ tracking_secret = abcdefghijklmnopqrstuvwxyz0123456789
## -- Self-service sponsored link stuff --
# (secure) payment domain
-payment_domain = http://pay.localhost/
-ad_domain = http://localhost
+payment_domain = http://reddit.local/
+ad_domain = http://reddit.local
+allowed_pay_countries = United States, United Kingdom, Canada
sponsors =
-# authorize.net credentials
+
+# authorize.net credentials (blank authorizenetapi to disable)
+authorizenetapi =
+# authorizenetapi = https://api.authorize.net/xml/v1/request.api
authorizenetname =
authorizenetkey =
-authorizenetapi = https://api.authorize.net/xml/v1/request.api
+
min_promote_bid = 20
max_promote_bid = 9999
min_promote_future = 2
@@ -227,7 +242,7 @@ authorized_cnames =
num_query_queue_workers = 5
query_queue_worker = http://cslowe.local:8000
enable_doquery = True
-use_query_cache = False
+use_query_cache = True
write_query_queue = True
# -- stylesheet editor --
@@ -243,6 +258,9 @@ stylesheet_rtl = reddit-rtl.css
# location of the static directory
static_path = /static/
+# make frontpage 100% dart
+frontpage_dart = false
+
# -- translator UI --
# enable/disable access to the translation UI in /admin/i18n
translator = true
diff --git a/r2/r2/config/middleware.py b/r2/r2/config/middleware.py
index 709e75e53f..ae731d4128 100644
--- a/r2/r2/config/middleware.py
+++ b/r2/r2/config/middleware.py
@@ -255,7 +255,7 @@ def filter(self, execution_func, prof_arg = None):
return [res]
class DomainMiddleware(object):
- lang_re = re.compile(r"^\w\w(-\w\w)?$")
+ lang_re = re.compile(r"\A\w\w(-\w\w)?\Z")
def __init__(self, app):
self.app = app
@@ -371,7 +371,7 @@ def __call__(self, environ, start_response):
return self.app(environ, start_response)
class DomainListingMiddleware(object):
- domain_pattern = re.compile(r'^/domain/(([-\w]+\.)+[\w]+)')
+ domain_pattern = re.compile(r'\A/domain/(([-\w]+\.)+[\w]+)')
def __init__(self, app):
self.app = app
@@ -386,7 +386,7 @@ def __call__(self, environ, start_response):
return self.app(environ, start_response)
class ExtensionMiddleware(object):
- ext_pattern = re.compile(r'\.([^/]+)$')
+ ext_pattern = re.compile(r'\.([^/]+)\Z')
extensions = (('rss' , ('xml', 'text/xml; charset=UTF-8')),
('xml' , ('xml', 'text/xml; charset=UTF-8')),
diff --git a/r2/r2/config/rewrites.py b/r2/r2/config/rewrites.py
index 7e6dd728f3..3464023b29 100644
--- a/r2/r2/config/rewrites.py
+++ b/r2/r2/config/rewrites.py
@@ -23,9 +23,9 @@
rewrites = (#these first two rules prevent the .embed rewrite from
#breaking other js that should work
- ("^/_(.*)", "/_$1"),
- ("^/static/(.*\.js)", "/static/$1"),
+ ("\A/_(.*)", "/_$1"),
+ ("\A/static/(.*\.js)", "/static/$1"),
#This next rewrite makes it so that all the embed stuff works.
- ("^(.*)(?' % sn
- else:
- g.log.error("GOOGLE CHCEKOUT: didn't work")
- g.log.error(repr(list(request.POST.iteritems())))
-
-
-
@noresponse(VUser(),
VModhash(),
thing = VByName('id'))
@@ -1770,9 +1573,8 @@ def GET_bookmarklet(self, action, uh, links):
@validatedForm(VUser(),
- code = VPrintable("code", 30),
- postcard_okay = VOneOf("postcard", ("yes", "no")),)
- def POST_claimgold(self, form, jquery, code, postcard_okay):
+ code = VPrintable("code", 30))
+ def POST_claimgold(self, form, jquery, code):
if not code:
c.errors.add(errors.NO_TEXT, field = "code")
form.has_errors("code", errors.NO_TEXT)
@@ -1802,17 +1604,21 @@ def POST_claimgold(self, form, jquery, code, postcard_okay):
if subscr_id:
c.user.gold_subscr_id = subscr_id
- admintools.engolden(c.user, days)
+ if code.startswith("cr_"):
+ c.user.gold_creddits += int(days / 31)
+ c.user._commit()
+ form.set_html(".status", _("claimed! now go to someone's userpage and give them a present!"))
+ else:
+ admintools.engolden(c.user, days)
- g.cache.set("recent-gold-" + c.user.name, True, 600)
- form.set_html(".status", _("claimed!"))
- jquery(".lounge").show()
+ g.cache.set("recent-gold-" + c.user.name, True, 600)
+ form.set_html(".status", _("claimed!"))
+ jquery(".lounge").show()
# Activate any errors we just manually set
form.has_errors("code", errors.INVALID_CODE, errors.CLAIMED_CODE,
errors.NO_TEXT)
-
@validatedForm(user = VUserWithEmail('name'))
def POST_password(self, form, jquery, user):
if form.has_errors('name', errors.USER_DOESNT_EXIST):
@@ -1824,7 +1630,7 @@ def POST_password(self, form, jquery, user):
form.set_html(".status",
_("an email will be sent to that account's address shortly"))
-
+
@validatedForm(cache_evt = VCacheKey('reset', ('key',)),
password = VPassword(['passwd', 'passwd2']))
def POST_resetpassword(self, form, jquery, cache_evt, password):
@@ -1862,11 +1668,6 @@ def POST_frame(self):
c.user._commit()
-
- @validatedForm()
- def POST_new_captcha(self, form, jquery, *a, **kw):
- jquery("body").captcha(get_iden())
-
@noresponse(VAdmin(),
tr = VTranslation("lang"),
user = nop('user'))
diff --git a/r2/r2/controllers/embed.py b/r2/r2/controllers/embed.py
index 6ac0e9701a..589050fa2f 100644
--- a/r2/r2/controllers/embed.py
+++ b/r2/r2/controllers/embed.py
@@ -23,6 +23,7 @@
from r2.lib.template_helpers import get_domain
from r2.lib.pages import Embed, BoringPage, HelpPage
from r2.lib.filters import websafe, SC_OFF, SC_ON
+from r2.lib.memoize import memoize
from pylons.i18n import _
from pylons import c, g, request
@@ -30,6 +31,22 @@
from urllib2 import HTTPError
+@memoize("renderurl_cached", time=60)
+def renderurl_cached(path):
+ # Needed so http://reddit.com/help/ works
+ fp = path.rstrip("/")
+ u = "http://code.reddit.com/wiki" + fp + '?stripped=1'
+
+ g.log.debug("Pulling %s for help" % u)
+
+ try:
+ return fp, proxyurl(u)
+ except HTTPError, e:
+ if e.code != 404:
+ print "error %s" % e.code
+ print e.fp.read()
+ return (None, None)
+
class EmbedController(RedditController):
allow_stylesheets = True
@@ -73,20 +90,10 @@ def renderurl(self, override=None):
else:
path = request.path
- # Needed so http://reddit.com/help/ works
- fp = path.rstrip("/")
- u = "http://code.reddit.com/wiki" + fp + '?stripped=1'
-
- g.log.debug("Pulling %s for help" % u)
-
- try:
- content = proxyurl(u)
- return self.rendercontent(content, fp)
- except HTTPError, e:
- if e.code != 404:
- print "error %s" % e.code
- print e.fp.read()
- return self.abort404()
+ fp, content = renderurl_cached(path)
+ if content is None:
+ self.abort404()
+ return self.rendercontent(content, fp)
GET_help = POST_help = renderurl
diff --git a/r2/r2/controllers/error.py b/r2/r2/controllers/error.py
index 02e95835e3..4fc40a9e29 100644
--- a/r2/r2/controllers/error.py
+++ b/r2/r2/controllers/error.py
@@ -119,7 +119,7 @@ def send404(self):
c.response.status_code = 404
if 'usable_error_content' in request.environ:
return request.environ['usable_error_content']
- if c.site._spam and not c.user_is_admin:
+ if c.site.spammy() and not c.user_is_admin:
subject = ("the subreddit /r/%s has been incorrectly banned" %
c.site.name)
lnk = ("/r/redditrequest/submit?url=%s&title=%s"
diff --git a/r2/r2/controllers/errors.py b/r2/r2/controllers/errors.py
index b01e6efdb2..f77047cfdb 100644
--- a/r2/r2/controllers/errors.py
+++ b/r2/r2/controllers/errors.py
@@ -56,7 +56,7 @@
('SUBREDDIT_NOTALLOWED', _("you aren't allowed to post there.")),
('SUBREDDIT_REQUIRED', _('you must specify a reddit')),
('BAD_SR_NAME', _('that name isn\'t going to work')),
- ('RATELIMIT', _('you are trying to submit too fast. try again in %(time)s.')),
+ ('RATELIMIT', _('you are doing that too much. try again in %(time)s.')),
('EXPIRED', _('your session has expired')),
('DRACONIAN', _('you must accept the terms first')),
('BANNED_IP', "IP banned"),
diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py
index 33a1026ef8..19e225ccb9 100644
--- a/r2/r2/controllers/front.py
+++ b/r2/r2/controllers/front.py
@@ -30,6 +30,7 @@
from r2.lib.menus import *
from r2.lib.utils import to36, sanitize_url, check_cheating, title_to_url
from r2.lib.utils import query_string, UrlParser, link_from_url, link_duplicates
+from r2.lib.utils import randstr
from r2.lib.template_helpers import get_domain
from r2.lib.filters import unsafe
from r2.lib.emailer import has_opted_out, Email
@@ -37,7 +38,7 @@
from r2.lib.db import queries
from r2.lib.strings import strings
from r2.lib.solrsearch import RelatedSearchQuery, SubredditSearchQuery
-from r2.lib.indextank import IndextankQuery, IndextankException
+from r2.lib.indextank import IndextankQuery, IndextankException, InvalidIndextankQuery
from r2.lib.contrib.pysolr import SolrError
from r2.lib import jsontemplates
from r2.lib import sup
@@ -45,6 +46,7 @@
from listingcontroller import ListingController
from pylons import c, request, request, Response
+import string
import random as rand
import re, socket
import time as time_module
@@ -251,6 +253,8 @@ def GET_comments(self, article, comment, context, sort, limit, depth):
if comment:
displayPane.append(PermalinkMessage(article.make_permalink_slow()))
+ displayPane.append(LinkCommentSep())
+
# insert reply box only for logged in user
if c.user_is_loggedin and can_comment_link(article) and not is_api():
#no comment box for permalinks
@@ -274,7 +278,6 @@ def GET_comments(self, article, comment, context, sort, limit, depth):
# Used in template_helpers
c.previous_visits = previous_visits
-
# finally add the comment listing
displayPane.append(CommentPane(article, CommentSortMenu.operator(sort),
comment, context, num, **kw))
@@ -569,7 +572,7 @@ def GET_search_reddits(self, query, reverse, after, count, num):
simple=True).render()
return res
- verify_langs_regex = re.compile(r"^[a-z][a-z](,[a-z][a-z])*$")
+ verify_langs_regex = re.compile(r"\A[a-z][a-z](,[a-z][a-z])*\Z")
@base_listing
@validate(query = nop('q'),
sort = VMenu('sort', SearchSortMenu, remember=False),
@@ -587,20 +590,37 @@ def GET_search(self, query, num, reverse, after, count, sort, restrict_sr):
site = c.site
try:
- q = IndextankQuery(query, site, sort)
-
- num, t, spane = self._search(q, num = num, after = after, reverse = reverse,
- count = count)
+ cleanup_message = None
+ try:
+ q = IndextankQuery(query, site, sort)
+ num, t, spane = self._search(q, num=num, after=after,
+ reverse = reverse, count = count)
+ except InvalidIndextankQuery:
+ # delete special characters from the query and run again
+ special_characters = '+-&|!(){}[]^"~*?:\\'
+ translation = dict((ord(char), None)
+ for char in list(special_characters))
+ cleaned = query.translate(translation)
+
+ q = IndextankQuery(cleaned, site, sort)
+ num, t, spane = self._search(q, num=num, after=after,
+ reverse = reverse, count = count)
+ cleanup_message = _('I couldn\'t understand your query, ' +
+ 'so I simplified it and searched for ' +
+ '"%(clean_query)s" instead.') % {
+ 'clean_query': cleaned }
+
res = SearchPage(_('search results'), query, t, num, content=spane,
nav_menus = [SearchSortMenu(default=sort)],
- search_params = dict(sort = sort),
- simple=False, site=c.site, restrict_sr=restrict_sr).render()
+ search_params = dict(sort = sort),
+ infotext=cleanup_message,
+ simple=False, site=c.site,
+ restrict_sr=restrict_sr).render()
return res
except (IndextankException, socket.error), e:
return self.search_fail(e)
-
def _search(self, query_obj, num, after, reverse, count=0):
"""Helper function for interfacing with search. Basically a
thin wrapper for SearchBuilder."""
@@ -983,3 +1003,64 @@ def GET_try_compact(self, dest):
def GET_thanks(self, secret):
"""The page to claim reddit gold trophies"""
return BoringPage(_("thanks"), content=Thanks(secret)).render()
+
+ @validate(VUser(),
+ goldtype = VOneOf("goldtype",
+ ("autorenew", "onetime", "creddits", "gift")),
+ period = VOneOf("period", ("monthly", "yearly")),
+ months = VInt("months"),
+ # variables below are just for gifts
+ signed = VBoolean("signed"),
+ recipient_name = VPrintable("recipient", max_length = 50),
+ giftmessage = VLength("giftmessage", 10000))
+ def GET_gold(self, goldtype, period, months,
+ signed, recipient_name, giftmessage):
+ start_over = False
+ recipient = None
+ if goldtype == "autorenew":
+ if period is None:
+ start_over = True
+ elif goldtype in ("onetime", "creddits"):
+ if months is None or months < 1:
+ start_over = True
+ elif goldtype == "gift":
+ if months is None or months < 1:
+ start_over = True
+ try:
+ recipient = Account._by_name(recipient_name or "")
+ except NotFound:
+ start_over = True
+ else:
+ goldtype = ""
+ start_over = True
+
+ if start_over:
+ return BoringPage(_("reddit gold"),
+ show_sidebar = False,
+ content=Gold(goldtype, period, months, signed,
+ recipient, recipient_name)).render()
+ else:
+ payment_blob = dict(goldtype = goldtype,
+ account_id = c.user._id,
+ account_name = c.user.name,
+ status = "initialized")
+
+ if goldtype == "gift":
+ payment_blob["signed"] = signed
+ payment_blob["recipient"] = recipient_name
+ payment_blob["giftmessage"] = giftmessage
+
+ passthrough = randstr(15)
+
+ g.hardcache.set("payment_blob-" + passthrough,
+ payment_blob, 86400 * 30)
+
+ g.log.info("just set payment_blob-%s" % passthrough)
+
+ return BoringPage(_("reddit gold"),
+ show_sidebar = False,
+ content=GoldPayment(goldtype, period, months,
+ signed, recipient,
+ giftmessage, passthrough)
+ ).render()
+
diff --git a/r2/r2/controllers/ipn.py b/r2/r2/controllers/ipn.py
new file mode 100644
index 0000000000..cac5306c3b
--- /dev/null
+++ b/r2/r2/controllers/ipn.py
@@ -0,0 +1,476 @@
+from xml.dom.minidom import Document
+from httplib import HTTPSConnection
+from urlparse import urlparse
+import base64
+
+from pylons.controllers.util import abort
+from pylons import c, g, response
+from pylons.i18n import _
+
+from validator import *
+from r2.models import *
+
+from reddit_base import RedditController
+
+def get_blob(code):
+ key = "payment_blob-" + code
+ with g.make_lock("payment_blob_lock-" + code):
+ blob = g.hardcache.get(key)
+ if not blob:
+ raise NotFound("No payment_blob-" + code)
+ if blob.get('status', None) != 'initialized':
+ raise ValueError("payment_blob %s has status = %s" %
+ (code, blob.get('status', None)))
+ blob['status'] = "locked"
+ g.hardcache.set(key, blob, 86400 * 30)
+ return key, blob
+
+def dump_parameters(parameters):
+ for k, v in parameters.iteritems():
+ g.log.info("IPN: %r = %r" % (k, v))
+
+def check_payment_status(payment_status):
+ if payment_status is None:
+ payment_status = ''
+
+ psl = payment_status.lower()
+
+ if psl == 'completed':
+ return (None, psl)
+ elif psl == 'refunded':
+ log_text("refund", "Just got notice of a refund.", "info")
+ # TODO: something useful when this happens -- and don't
+ # forget to verify first
+ return ("Ok", psl)
+ elif psl == 'pending':
+ log_text("pending",
+ "Just got notice of a Pending, whatever that is.", "info")
+ # TODO: something useful when this happens -- and don't
+ # forget to verify first
+ return ("Ok", psl)
+ elif psl == 'reversed':
+ log_text("reversal",
+ "Just got notice of a PayPal reversal.", "info")
+ # TODO: something useful when this happens -- and don't
+ # forget to verify first
+ return ("Ok", psl)
+ elif psl == 'canceled_reversal':
+ log_text("canceled_reversal",
+ "Just got notice of a PayPal 'canceled reversal'.", "info")
+ return ("Ok", psl)
+ elif psl == '':
+ return (None, psl)
+ else:
+ raise ValueError("Unknown IPN status: %r" % payment_status)
+
+def check_txn_type(txn_type, psl):
+ if txn_type == 'subscr_signup':
+ return ("Ok", None)
+ elif txn_type == 'subscr_cancel':
+ return ("Ok", "cancel")
+ elif txn_type == 'subscr_eot':
+ return ("Ok", None)
+ elif txn_type == 'subscr_failed':
+ log_text("failed_subscription",
+ "Just got notice of a failed PayPal resub.", "info")
+ return ("Ok", None)
+ elif txn_type == 'subscr_modify':
+ log_text("modified_subscription",
+ "Just got notice of a modified PayPal sub.", "info")
+ return ("Ok", None)
+ elif txn_type == 'send_money':
+ return ("Ok", None)
+ elif txn_type in ('new_case',
+ 'recurring_payment_suspended_due_to_max_failed_payment'):
+ return ("Ok", None)
+ elif txn_type == 'subscr_payment' and psl == 'completed':
+ return (None, "new")
+ elif txn_type == 'web_accept' and psl == 'completed':
+ return (None, None)
+ else:
+ raise ValueError("Unknown IPN txn_type / psl %r" %
+ ((txn_type, psl),))
+
+
+def verify_ipn(parameters):
+ paraemeters['cmd'] = '_notify-validate'
+ try:
+ safer = dict([k, v.encode('utf-8')] for k, v in parameters.items())
+ params = urllib.urlencode(safer)
+ except UnicodeEncodeError:
+ g.log.error("problem urlencoding %r" % (parameters,))
+ raise
+ req = urllib2.Request(g.PAYPAL_URL, params)
+ req.add_header("Content-type", "application/x-www-form-urlencoded")
+
+ response = urllib2.urlopen(req)
+ status = response.read()
+
+ if status != "VERIFIED":
+ raise ValueError("Invalid IPN response: %r" % status)
+
+
+def existing_subscription(subscr_id):
+ account_id = accountid_from_paypalsubscription(subscr_id)
+
+ if account_id is None:
+ return None
+
+ try:
+ account = Account._byID(account_id)
+ except NotFound:
+ g.log.info("Just got IPN renewal for deleted account #%d"
+ % account_id)
+ return "deleted account"
+
+ return account
+
+def months_and_days_from_pennies(pennies):
+ if pennies >= 2999:
+ months = 12 * (pennies / 2999)
+ days = 366 * (pennies / 2999)
+ else:
+ months = pennies / 399
+ days = 31 * months
+ return (months, days)
+
+def send_gift(buyer, recipient, months, days, signed, giftmessage):
+ admintools.engolden(recipient, days)
+ if signed:
+ sender = buyer.name
+ md_sender = "[%s](/user/%s)" % (sender, sender)
+ else:
+ sender = "someone"
+ md_sender = "An anonymous redditor"
+
+ create_gift_gold (buyer._id, recipient._id, days, c.start_time, signed)
+ if months == 1:
+ amount = "a month"
+ else:
+ amount = "%d months" % months
+
+ subject = sender + " just sent you reddit gold!"
+ message = strings.youve_got_gold % dict(sender=md_sender, amount=amount)
+
+ if giftmessage and giftmessage.strip():
+ message += "\n\n" + strings.giftgold_note + giftmessage
+
+ send_system_message(recipient, subject, message)
+
+ g.log.info("%s gifted %s to %s" % (buyer.name, amount, recipient.name))
+
+def _google_ordernum_request(ordernums):
+ d = Document()
+ n = d.createElement("notification-history-request")
+ n.setAttribute("xmlns", "http://checkout.google.com/schema/2")
+ d.appendChild(n)
+
+ on = d.createElement("order-numbers")
+ n.appendChild(on)
+
+ for num in tup(ordernums):
+ gon = d.createElement('google-order-number')
+ gon.appendChild(d.createTextNode("%s" % num))
+ on.appendChild(gon)
+
+ return _google_checkout_post(g.GOOGLE_REPORT_URL, d.toxml("UTF-8"))
+
+def _google_charge_and_ship(ordernum):
+ d = Document()
+ n = d.createElement("charge-and-ship-order")
+ n.setAttribute("xmlns", "http://checkout.google.com/schema/2")
+ n.setAttribute("google-order-number", ordernum)
+
+ d.appendChild(n)
+
+ return _google_checkout_post(g.GOOGLE_REQUEST_URL, d.toxml("UTF-8"))
+
+
+def _google_checkout_post(url, params):
+ u = urlparse("%s%s" % (url, g.GOOGLE_ID))
+ conn = HTTPSConnection(u.hostname, u.port)
+ auth = base64.encodestring('%s:%s' % (g.GOOGLE_ID, g.GOOGLE_KEY))[:-1]
+ headers = {"Authorization": "Basic %s" % auth,
+ "Content-type": "text/xml; charset=\"UTF-8\""}
+
+ conn.request("POST", u.path, params, headers)
+ response = conn.getresponse().read()
+ conn.close()
+
+ return BeautifulStoneSoup(response)
+
+class IpnController(RedditController):
+ # Used when buying gold with creddits
+ @validatedForm(VUser(),
+ months = VInt("months"),
+ passthrough = VPrintable("passthrough", max_length=50))
+ def POST_spendcreddits(self, form, jquery, months, passthrough):
+ if months is None or months < 1:
+ form.set_html(".status", _("nice try."))
+ return
+
+ days = months * 31
+
+ if not passthrough:
+ raise ValueError("/spendcreddits got no passthrough?")
+
+ blob_key, payment_blob = get_blob(passthrough)
+ if payment_blob["goldtype"] != "gift":
+ raise ValueError("/spendcreddits payment_blob %s has goldtype %s" %
+ (passthrough, payment_blob["goldtype"]))
+
+ signed = payment_blob["signed"]
+ giftmessage = payment_blob["giftmessage"]
+ recipient_name = payment_blob["recipient"]
+
+ if payment_blob["account_id"] != c.user._id:
+ fmt = ("/spendcreddits payment_blob %s has userid %d " +
+ "but c.user._id is %d")
+ raise ValueError(fmt % passthrough,
+ payment_blob["account_id"],
+ c.user._id)
+
+ try:
+ recipient = Account._by_name(recipient_name)
+ except NotFound:
+ raise ValueError("Invalid username %s in spendcreddits, buyer = %s"
+ % (recipient_name, c.user.name))
+
+ if not c.user_is_admin:
+ if months > c.user.gold_creddits:
+ raise ValueError("%s is trying to sneak around the creddit check"
+ % c.user.name)
+
+ c.user.gold_creddits -= months
+ c.user.gold_creddit_escrow += months
+ c.user._commit()
+
+ send_gift(c.user, recipient, months, days, signed, giftmessage)
+
+ if not c.user_is_admin:
+ c.user.gold_creddit_escrow -= months
+ c.user._commit()
+
+ payment_blob["status"] = "processed"
+ g.hardcache.set(blob_key, payment_blob, 86400 * 30)
+
+ form.set_html(".status", _("the gold has been delivered!"))
+ jquery("button").hide()
+
+ @textresponse(full_sn = VLength('serial-number', 100))
+ def POST_gcheckout(self, full_sn):
+ if full_sn:
+ short_sn = full_sn.split('-')[0]
+ g.log.error( "GOOGLE CHECKOUT: %s" % short_sn)
+ trans = _google_ordernum_request(short_sn)
+
+ # get the financial details
+ auth = trans.find("authorization-amount-notification")
+
+ if not auth:
+ # see if the payment was declinded
+ status = trans.findAll('financial-order-state')
+ if 'PAYMENT_DECLINED' in [x.contents[0] for x in status]:
+ g.log.error("google declined transaction found: '%s'" %
+ short_sn)
+ elif 'REVIEWING' not in [x.contents[0] for x in status]:
+ g.log.error(("google transaction not found: " +
+ "'%s', status: %s")
+ % (short_sn, [x.contents[0] for x in status]))
+ else:
+ g.log.error(("google transaction status: " +
+ "'%s', status: %s")
+ % (short_sn, [x.contents[0] for x in status]))
+ elif auth.find("financial-order-state"
+ ).contents[0] == "CHARGEABLE":
+ email = str(auth.find("email").contents[0])
+ payer_id = str(auth.find('buyer-id').contents[0])
+ # get the "secret"
+ custom = None
+ cart = trans.find("shopping-cart")
+ if cart:
+ for item in cart.findAll("merchant-private-item-data"):
+ custom = str(item.contents[0])
+ break
+ if custom:
+ days = None
+ try:
+ pennies = int(float(trans.find("order-total"
+ ).contents[0])*100)
+ months, days = months_and_days_from_pennies(pennies)
+ charged = trans.find("charge-amount-notification")
+ if not charged:
+ _google_charge_and_ship(short_sn)
+
+ parameters = request.POST.copy()
+ self.finish(parameters, "g%s" % short_sn,
+ email, payer_id, None,
+ custom, pennies, months, days)
+ except ValueError, e:
+ g.log.error(e)
+ else:
+ raise ValueError("Got no custom blob for %s" % short_sn)
+
+ return (('') % full_sn)
+ else:
+ g.log.error("GOOGLE CHCEKOUT: didn't work")
+ g.log.error(repr(list(request.POST.iteritems())))
+
+ @textresponse(paypal_secret = VPrintable('secret', 50),
+ payment_status = VPrintable('payment_status', 20),
+ txn_id = VPrintable('txn_id', 20),
+ paying_id = VPrintable('payer_id', 50),
+ payer_email = VPrintable('payer_email', 250),
+ mc_currency = VPrintable('mc_currency', 20),
+ mc_gross = VFloat('mc_gross'),
+ custom = VPrintable('custom', 50))
+ def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id,
+ payer_email, mc_currency, mc_gross, custom):
+
+ parameters = request.POST.copy()
+
+ # Make sure it's really PayPal
+ if paypal_secret != g.PAYPAL_SECRET:
+ log_text("invalid IPN secret",
+ "%s guessed the wrong IPN secret" % request.ip,
+ "warning")
+ raise ValueError
+
+ # Return early if it's an IPN class we don't care about
+ response, psl = check_payment_status(payment_status)
+ if response:
+ return response
+
+ # Return early if it's a txn_type we don't care about
+ response, subscription = check_txn_type(parameters['txn_type'], psl)
+ if subscription is None:
+ subscr_id = None
+ elif subscription == "new":
+ subscr_id = parameters['subscr_id']
+ elif subscription == "cancel":
+ cancel_subscription(parameters['subscr_id'])
+ else:
+ raise ValueError("Weird subscription: %r" % subscription)
+
+ if response:
+ return response
+
+ # Check for the debug flag, and if so, dump the IPN dict
+ if g.cache.get("ipn-debug"):
+ g.cache.delete("ipn-debug")
+ dump_parameters(parameters)
+
+ # More sanity checks...
+ if False: # TODO: remove this line
+ verify_ipn(parameters)
+
+ if mc_currency != 'USD':
+ raise ValueError("Somehow got non-USD IPN %r" % mc_currency)
+
+ if not (txn_id and paying_id and payer_email and mc_gross):
+ dump_parameters(parameters)
+ raise ValueError("Got incomplete IPN")
+
+ pennies = int(mc_gross * 100)
+ months, days = months_and_days_from_pennies(pennies)
+
+ # Special case: autorenewal payment
+ existing = existing_subscription(subscr_id)
+ if existing:
+ if existing != "deleted account":
+ create_claimed_gold ("P" + txn_id, payer_email, paying_id,
+ pennies, days, None, existing._id,
+ c.start_time, subscr_id)
+ admintools.engolden(existing, days)
+
+ g.log.info("Just applied IPN renewal for %s, %d days" %
+ (existing.name, days))
+ return "Ok"
+ elif subscr_id:
+ g.log.warning("IPN subscription %s is not associated with anyone"
+ % subscr_id)
+
+ # More sanity checks that all non-autorenewals should pass:
+
+ if not custom:
+ dump_parameters(parameters)
+ raise ValueError("Got IPN with txn_id=%s and no custom"
+ % txn_id)
+
+ self.finish(parameters, "P" + txn_id,
+ payer_email, paying_id, subscr_id,
+ custom, pennies, months, days)
+
+ def finish(self, parameters, txn_id,
+ payer_email, paying_id, subscr_id,
+ custom, pennies, months, days):
+
+ blob_key, payment_blob = get_blob(custom)
+
+ buyer_id = payment_blob.get('account_id', None)
+ if not buyer_id:
+ dump_parameters(parameters)
+ raise ValueError("No buyer_id in IPN/GC with custom='%s'" % custom)
+ try:
+ buyer = Account._byID(buyer_id)
+ except NotFound:
+ dump_parameters(parameters)
+ raise ValueError("Invalid buyer_id %d in IPN/GC with custom='%s'"
+ % (buyer_id, custom))
+
+ if subscr_id:
+ buyer.gold_subscr_id = subscr_id
+
+ instagift = False
+ if payment_blob['goldtype'] in ('autorenew', 'onetime'):
+ admintools.engolden(buyer, days)
+
+ subject = _("thanks for buying reddit gold!")
+
+ if g.lounge_reddit:
+ lounge_url = "/r/" + g.lounge_reddit
+ message = strings.lounge_msg % dict(link=lounge_url)
+ else:
+ message = ":)"
+ elif payment_blob['goldtype'] == 'creddits':
+ buyer._incr("gold_creddits", months)
+ buyer._commit()
+ subject = _("thanks for buying creddits!")
+ message = _("To spend them, visit [/gold](/gold) or your favorite person's userpage.")
+ elif payment_blob['goldtype'] == 'gift':
+ recipient_name = payment_blob.get('recipient', None)
+ try:
+ recipient = Account._by_name(recipient_name)
+ except NotFound:
+ dump_parameters(parameters)
+ raise ValueError("Invalid recipient_name %s in IPN/GC with custom='%s'"
+ % (recipient_name, custom))
+ signed = payment_blob.get("signed", False)
+ giftmessage = payment_blob.get("giftmessage", False)
+ send_gift(buyer, recipient, months, days, signed, giftmessage)
+ instagift = True
+ subject = _("thanks for giving reddit gold!")
+ message = _("Your gift to %s has been delivered." % recipient.name)
+ else:
+ dump_parameters(parameters)
+ raise ValueError("Got status '%s' in IPN/GC" % payment_blob['status'])
+
+ # Reuse the old "secret" column as a place to record the goldtype
+ # and "custom", just in case we need to debug it later or something
+ secret = payment_blob['goldtype'] + "-" + custom
+
+ if instagift:
+ status="instagift"
+ else:
+ status="processed"
+
+ create_claimed_gold(txn_id, payer_email, paying_id, pennies, days,
+ secret, buyer_id, c.start_time,
+ subscr_id, status=status)
+
+ send_system_message(buyer, subject, message)
+
+ payment_blob["status"] = "processed"
+ g.hardcache.set(blob_key, payment_blob, 86400 * 30)
diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py
index 176e501787..181256a37c 100644
--- a/r2/r2/controllers/listingcontroller.py
+++ b/r2/r2/controllers/listingcontroller.py
@@ -147,6 +147,8 @@ def keep(item):
wouldkeep = item.keep_item(item)
if getattr(item, "promoted", None) is not None:
return False
+ if item._deleted and not c.user_is_admin:
+ return False
return wouldkeep
return keep
@@ -398,7 +400,7 @@ def keep(item):
@property
def menus(self):
return [ControversyTimeMenu(default = self.time)]
-
+
def query(self):
return c.site.get_links(self.sort, self.time)
@@ -453,14 +455,14 @@ def GET_listing(self, links, **env):
#class RecommendedController(ListingController):
# where = 'recommended'
# title_text = _('recommended for you')
-#
+#
# @property
# def menus(self):
# return [RecSortMenu(default = self.sort)]
-#
+#
# def query(self):
# return get_recommended(c.user._id, sort = self.sort)
-#
+#
# @validate(VUser(),
# sort = VMenu("controller", RecSortMenu))
# def GET_listing(self, sort, **env):
@@ -497,7 +499,8 @@ def keep_fn(self):
# keep promotions off of profile pages.
def keep(item):
wouldkeep = True
- if item._deleted:
+ # TODO: Consider a flag to disable this (and see below plus builder.py)
+ if item._deleted and not c.user_is_admin:
return False
if self.time != 'all':
wouldkeep = (item._date > utils.timeago('1 %s' % str(self.time)))
@@ -618,25 +621,26 @@ def title(self):
def keep_fn(self):
def keep(item):
wouldkeep = item.keep_item(item)
- if item._deleted or item._spam:
+ # TODO: Consider a flag to disable this (and see above plus builder.py)
+ if (item._deleted or item._spam) and not c.user_is_admin:
return False
# don't show user their own unread stuff
if ((self.where == 'unread' or self.subwhere == 'unread')
and (item.author_id == c.user._id or not item.new)):
return False
+
return wouldkeep
return keep
@staticmethod
def builder_wrapper(thing):
if isinstance(thing, Comment):
- p = thing.make_permalink_slow()
f = thing._fullname
w = Wrapped(thing)
w.render_class = Message
w.to_id = c.user._id
w.was_comment = True
- w.permalink, w._fullname = p, f
+ w._fullname = f
else:
w = ListingController.builder_wrapper(thing)
diff --git a/r2/r2/controllers/post.py b/r2/r2/controllers/post.py
index c061e4d825..4ef8b0f1f7 100644
--- a/r2/r2/controllers/post.py
+++ b/r2/r2/controllers/post.py
@@ -97,6 +97,7 @@ def POST_unlogged_options(self, all_langs, pref_lang):
pref_private_feeds = VBoolean("private_feeds"),
pref_show_adbox = VBoolean("show_adbox"),
pref_show_sponsors = VBoolean("show_sponsors"),
+ pref_show_sponsorships = VBoolean("show_sponsorships"),
pref_highlight_new_comments = VBoolean("highlight_new_comments"),
all_langs = nop('all-langs', default = 'all'))
def POST_options(self, all_langs, pref_lang, **kw):
@@ -117,6 +118,7 @@ def POST_options(self, all_langs, pref_lang, **kw):
if not c.user.gold:
kw['pref_show_adbox'] = True
kw['pref_show_sponsors'] = True
+ kw['pref_show_sponsorships'] = True
self.set_options(all_langs, pref_lang, **kw)
u = UrlParser(c.site.path + "prefs")
diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py
index 45f27a8532..097ca76655 100644
--- a/r2/r2/controllers/promotecontroller.py
+++ b/r2/r2/controllers/promotecontroller.py
@@ -406,10 +406,10 @@ def POST_rm_traffic_viewer(self, form, jquery, iuser, thing):
customer_id = VInt("customer_id", min = 0),
pay_id = VInt("account", min = 0),
edit = VBoolean("edit"),
- address = ValidAddress(["firstName", "lastName",
- "company", "address",
- "city", "state", "zip",
- "country", "phoneNumber"]),
+ address = ValidAddress(
+ ["firstName", "lastName", "company", "address",
+ "city", "state", "zip", "country", "phoneNumber"],
+ allowed_countries = g.allowed_pay_countries),
creditcard = ValidCard(["cardNumber", "expirationDate",
"cardCode"]))
def POST_update_pay(self, form, jquery, link, indx, customer_id, pay_id,
@@ -423,13 +423,18 @@ def POST_update_pay(self, form, jquery, link, indx, customer_id, pay_id,
form.has_errors(["cardNumber", "expirationDate", "cardCode"],
errors.BAD_CARD)):
pass
- else:
+ elif g.authorizenetapi:
pay_id = edit_profile(c.user, address, creditcard, pay_id)
+ else:
+ pay_id = 1
# if link is in use or finished, don't make a change
if pay_id:
# valid bid and created or existing bid id.
# check if already a transaction
- success, reason = promote.auth_campaign(link, indx, c.user, pay_id)
+ if g.authorizenetapi:
+ success, reason = promote.auth_campaign(link, indx, c.user, pay_id)
+ else:
+ success = True
if success:
form.redirect(promote.promo_edit_url(link))
else:
@@ -449,10 +454,14 @@ def GET_pay(self, article, indx):
if indx not in getattr(article, "campaigns", {}):
return self.abort404()
- data = get_account_info(c.user)
- content = PaymentForm(article, indx,
- customer_id = data.customerProfileId,
- profiles = data.paymentProfiles)
+ if g.authorizenetapi:
+ data = get_account_info(c.user)
+ content = PaymentForm(article, indx,
+ customer_id = data.customerProfileId,
+ profiles = data.paymentProfiles)
+ else:
+ content = PaymentForm(article, 0, customer_id = 0,
+ profiles = [])
res = LinkInfoPage(link = article,
content = content,
show_sidebar = False)
diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py
index 9bf7c2f026..35e6ae3bed 100644
--- a/r2/r2/controllers/reddit_base.py
+++ b/r2/r2/controllers/reddit_base.py
@@ -50,6 +50,7 @@
from pylons import Response
NEVER = 'Thu, 31 Dec 2037 23:59:59 GMT'
+DELETE = 'Thu, 01-Jan-1970 00:00:01 GMT'
cache_affecting_cookies = ('reddit_first','over18','_options')
@@ -240,6 +241,8 @@ def set_subreddit():
sr_name = request.environ.get("subreddit", request.POST.get('r'))
domain = request.environ.get("domain")
+ can_stale = request.method.upper() in ('GET','HEAD')
+
default_sr = DefaultSR()
c.site = default_sr
if not sr_name:
@@ -256,7 +259,7 @@ def set_subreddit():
srs = set()
sr_names = sr_name.split('+')
real_path = sr_name
- srs = Subreddit._by_name(sr_names).values()
+ srs = Subreddit._by_name(sr_names, stale=can_stale).values()
if len(srs) != len(sr_names):
abort(404)
elif any(isinstance(sr, FakeSubreddit)
@@ -271,7 +274,7 @@ def set_subreddit():
sr_ids = [sr._id for sr in srs]
c.site = MultiReddit(sr_ids, real_path)
else:
- c.site = Subreddit._by_name(sr_name)
+ c.site = Subreddit._by_name(sr_name, stale=can_stale)
except NotFound:
sr_name = chksrname(sr_name)
if sr_name:
@@ -414,7 +417,7 @@ def set_recent_reddits():
def set_colors():
theme_rx = re.compile(r'')
- color_rx = re.compile(r'^([a-fA-F0-9]){3}(([a-fA-F0-9]){3})?$')
+ color_rx = re.compile(r'\A([a-fA-F0-9]){3}(([a-fA-F0-9]){3})?\Z')
c.theme = None
if color_rx.match(request.get.get('bgcolor') or ''):
c.bgcolor = request.get.get('bgcolor')
@@ -492,7 +495,7 @@ def request_key(self):
c.over18,
c.firsttime,
c.extension,
- c.render_style,
+ c.render_style,
cookies_key)
def cached_response(self):
@@ -503,7 +506,7 @@ def pre(self):
c.start_time = datetime.now(g.tz)
g.reset_caches()
- c.domain_prefix = request.environ.get("reddit-domain-prefix",
+ c.domain_prefix = request.environ.get("reddit-domain-prefix",
g.domain_prefix)
#check if user-agent needs a dose of rate-limiting
if not c.error_page:
@@ -522,9 +525,9 @@ def pre(self):
def try_pagecache(self):
#check content cache
- if not c.user_is_loggedin:
+ if request.method.upper() == 'GET' and not c.user_is_loggedin:
r = g.rendercache.get(self.request_key())
- if r and request.method == 'GET':
+ if r:
r, c.cookies = r
response = c.response
response.headers = r.headers
@@ -570,7 +573,7 @@ def post(self):
#return
#set content cache
if (g.page_cache_time
- and request.method == 'GET'
+ and request.method.upper() == 'GET'
and (not c.user_is_loggedin or c.allow_loggedin_cache)
and not c.used_cache
and response.status_code != 503
@@ -610,6 +613,12 @@ def post(self):
sampling_rate = g.usage_sampling,
action = action)
+ # this thread is probably going to be reused, but it could be
+ # a while before it is. So we might as well dump the cache in
+ # the mean time so that we don't have dead objects hanging
+ # around taking up memory
+ g.reset_caches()
+
def abort404(self):
abort(404, "not found")
@@ -634,7 +643,7 @@ def update_qstring(self, dict):
def api_wrapper(self, kw):
data = simplejson.dumps(kw)
- if request.method == "GET" and request.GET.get("callback"):
+ if request.method.upper() == "GET" and request.GET.get("callback"):
return "%s(%s)" % (websafe_json(request.GET.get("callback")),
websafe_json(data))
return self.sendstring(data)
@@ -647,10 +656,10 @@ class RedditController(MinimalController):
def login(user, admin = False, rem = False):
c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin),
expires = NEVER if rem else None)
-
+
@staticmethod
def logout(admin = False):
- c.cookies[g.login_cookie] = Cookie(value='')
+ c.cookies[g.login_cookie] = Cookie(value='', expires=DELETE)
def pre(self):
c.response_wrappers = []
@@ -699,7 +708,7 @@ def pre(self):
if not c.user._loaded:
c.user._load()
c.modhash = c.user.modhash()
- if request.method.lower() == 'get':
+ if request.method.upper() == 'GET':
read_mod_cookie()
if hasattr(c.user, 'msgtime') and c.user.msgtime:
c.have_messages = c.user.msgtime
@@ -739,13 +748,13 @@ def pre(self):
# check that the site is available:
- if c.site._spam and not c.user_is_admin and not c.error_page:
+ if c.site.spammy() and not c.user_is_admin and not c.error_page:
abort(404, "not found")
# check if the user has access to this subreddit
if not c.site.can_view(c.user) and not c.error_page:
abort(403, "forbidden")
-
+
#check over 18
if (c.site.over_18 and not c.over18 and
request.path not in ("/frame", "/over18")
diff --git a/r2/r2/controllers/toolbar.py b/r2/r2/controllers/toolbar.py
index 304a717abd..1e5ac9f27a 100644
--- a/r2/r2/controllers/toolbar.py
+++ b/r2/r2/controllers/toolbar.py
@@ -34,11 +34,12 @@
import string
# strips /r/foo/, /s/, or both
-strip_sr = re.compile('^/r/[a-zA-Z0-9_-]+')
-strip_s_path = re.compile('^/s/')
-leading_slash = re.compile('^/+')
-has_protocol = re.compile('^https?:')
-need_insert_slash = re.compile('^https?:/[^/]')
+strip_sr = re.compile('\A/r/[a-zA-Z0-9_-]+')
+strip_s_path = re.compile('\A/s/')
+leading_slash = re.compile('\A/+')
+has_protocol = re.compile('\A[a-zA-Z_-]+:')
+allowed_protocol = re.compile('\Ahttps?:')
+need_insert_slash = re.compile('\Ahttps?:/[^/]')
def demangle_url(path):
# there's often some URL mangling done by the stack above us, so
# let's clean up the URL before looking it up
@@ -46,7 +47,10 @@ def demangle_url(path):
path = strip_s_path.sub('', path)
path = leading_slash.sub("", path)
- if not has_protocol.match(path):
+ if has_protocol.match(path):
+ if not allowed_protocol.match(path):
+ return None
+ else:
path = 'http://%s' % path
if need_insert_slash.match(path):
@@ -203,12 +207,16 @@ def GET_toolbar(self, link, url):
if link:
res = link[0]
elif url:
+ url = demangle_url(url)
+ if not url: # also check for validity
+ return self.abort404()
+
res = FrameToolbar(link = None,
title = None,
url = url,
expanded = False)
else:
- self.abort404()
+ return self.abort404()
return spaceCompress(res.render())
@validate(link = VByName('id'))
diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py
index f950de13c4..f6ccb722bb 100644
--- a/r2/r2/controllers/validator/validator.py
+++ b/r2/r2/controllers/validator/validator.py
@@ -373,11 +373,11 @@ def run(self, limit):
return min(max(i, 1), 100)
class VCssMeasure(Validator):
- measure = re.compile(r"^\s*[\d\.]+\w{0,3}\s*$")
+ measure = re.compile(r"\A\s*[\d\.]+\w{0,3}\s*\Z")
def run(self, value):
return value if value and self.measure.match(value) else ''
-subreddit_rx = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_]{2,20}$")
+subreddit_rx = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_]{2,20}\Z")
def chksrname(x):
#notice the space before reddit.com
@@ -391,7 +391,7 @@ def chksrname(x):
class VLength(Validator):
- only_whitespace = re.compile(r"^\s*$", re.UNICODE)
+ only_whitespace = re.compile(r"\A\s*\Z", re.UNICODE)
def __init__(self, param, max_length,
empty_error = errors.NO_TEXT,
@@ -514,7 +514,7 @@ def fullname_regex(thing_cls = None, multiple = False):
pattern += r"_[0-9a-z]+"
if multiple:
pattern = r"(%s *,? *)+" % pattern
- return re.compile(r"^" + pattern + r"$")
+ return re.compile(r"\A" + pattern + r"\Z")
class VByName(Validator):
splitter = re.compile('[ ,]+')
@@ -750,7 +750,7 @@ def run(self, sr_name, link_type = None):
return sr
-pass_rx = re.compile(r"^.{3,20}$")
+pass_rx = re.compile(r"\A.{3,20}\Z")
def chkpass(x):
return x if x and pass_rx.match(x) else None
@@ -764,10 +764,14 @@ def run(self, password, verify):
else:
return password.encode('utf8')
-user_rx = re.compile(r"^[\w-]{3,20}$", re.UNICODE)
+user_rx = re.compile(r"\A[\w-]{3,20}\Z", re.UNICODE)
def chkuser(x):
+ if x is None:
+ return None
try:
+ if any(ch.isspace() for ch in x):
+ return None
return str(x) if user_rx.match(x) else None
except TypeError:
return None
@@ -879,6 +883,8 @@ def run(self, name):
s = Subreddit._by_name(name.strip('#'))
if isinstance(s, FakeSubreddit):
raise NotFound, "fake subreddit"
+ if s._spam:
+ raise NotFound, "banned community"
return s
except NotFound:
self.set_error(errors.SUBREDDIT_NOEXIST)
@@ -977,7 +983,7 @@ class VCssName(Validator):
returns a name iff it consists of alphanumeric characters and
possibly "-", and is below the length limit.
"""
- r_css_name = re.compile(r"^[a-zA-Z0-9\-]{1,100}$")
+ r_css_name = re.compile(r"\A[a-zA-Z0-9\-]{1,100}\Z")
def run(self, name):
if name and self.r_css_name.match(name):
return name
@@ -1058,6 +1064,55 @@ def ratelimit(self, rate_user = False, rate_ip = False, prefix = "rate_",
to_set['ip' + str(request.ip)] = expire_time
g.cache.set_multi(to_set, prefix = prefix, time = seconds)
+class VDelay(Validator):
+ def __init__(self, category, *a, **kw):
+ self.category = category
+ Validator.__init__(self, *a, **kw)
+
+ def run (self):
+ key = "VDelay-%s-%s" % (self.category, request.ip)
+ prev_violations = g.cache.get(key)
+ if prev_violations:
+ time = utils.timeuntil(prev_violations["expire_time"])
+ if prev_violations["expire_time"] > datetime.now(g.tz):
+ self.set_error(errors.RATELIMIT, {'time': time},
+ field='vdelay')
+
+ @classmethod
+ def record_violation(self, category, seconds = None, growfast=False):
+ if seconds is None:
+ seconds = g.RATELIMIT*60
+
+ key = "VDelay-%s-%s" % (category, request.ip)
+ prev_violations = g.memcache.get(key)
+ if prev_violations is None:
+ prev_violations = dict(count=0)
+
+ num_violations = prev_violations["count"]
+
+ if growfast:
+ multiplier = 3 ** num_violations
+ else:
+ multiplier = 1
+
+ max_duration = 8 * 3600
+ duration = min(seconds * multiplier, max_duration)
+
+ expire_time = (datetime.now(g.tz) +
+ timedelta(seconds = duration))
+
+ prev_violations["expire_time"] = expire_time
+ prev_violations["duration"] = duration
+ prev_violations["count"] += 1
+
+ with g.make_lock("lock-" + key, timeout=5, verbose=False):
+ existing = g.memcache.get(key)
+ if existing and existing["count"] > prev_violations["count"]:
+ g.log.warning("Tried to set %s to count=%d, but found existing=%d"
+ % (key, prev_violations["count"], existing["count"]))
+ else:
+ g.cache.set(key, prev_violations, max_duration)
+
class VCommentIDs(Validator):
#id_str is a comma separated list of id36's
def run(self, id_str):
@@ -1182,7 +1237,7 @@ def run(self, emails0):
class VCnameDomain(Validator):
- domain_re = re.compile(r'^([\w\-_]+\.)+[\w]+$')
+ domain_re = re.compile(r'\A([\w\-_]+\.)+[\w]+\Z')
def run(self, domain):
if (domain
@@ -1316,8 +1371,8 @@ def run(self, dest):
return "/"
class ValidAddress(Validator):
- def __init__(self, param, usa_only = True):
- self.usa_only = usa_only
+ def __init__(self, param, allowed_countries = ["United States"]):
+ self.allowed_countries = allowed_countries
Validator.__init__(self, param)
def set_error(self, msg, field):
@@ -1338,20 +1393,18 @@ def run(self, firstName, lastName, company, address,
self.set_error(_("please provide your state"), "state")
elif not zipCode:
self.set_error(_("please provide your zip or post code"), "zip")
- elif (not self.usa_only and
- (not country or not pycountry.countries.get(alpha2=country))):
+ elif not country:
self.set_error(_("please pick a country"), "country")
else:
- if self.usa_only:
- country = 'United States'
- else:
- country = pycountry.countries.get(alpha2=country).name
+ country = pycountry.countries.get(alpha2=country)
+ if country.name not in self.allowed_countries:
+ self.set_error(_("Our ToS don't cover your country (yet). Sorry."), "country")
return Address(firstName = firstName,
lastName = lastName,
company = company or "",
address = address,
city = city, state = state,
- zip = zipCode, country = country,
+ zip = zipCode, country = country.name,
phoneNumber = phoneNumber or "")
class ValidCard(Validator):
@@ -1377,7 +1430,7 @@ def run(self, cardNumber, expirationDate, cardCode):
cardCode = cardCode)
class VTarget(Validator):
- target_re = re.compile("^[\w_-]{3,20}$")
+ target_re = re.compile("\A[\w_-]{3,20}\Z")
def run(self, name):
if name and self.target_re.match(name):
return name
diff --git a/r2/r2/lib/amqp.py b/r2/r2/lib/amqp.py
index 8aef88e6cf..5f882376b5 100644
--- a/r2/r2/lib/amqp.py
+++ b/r2/r2/lib/amqp.py
@@ -181,6 +181,8 @@ def consume_items(queue, callback, verbose=True):
single items at a time. This is more efficient than
handle_items when the queue is likely to be occasionally empty
or if batching the received messages is not necessary."""
+ from pylons import c
+
chan = connection_manager.get_channel()
def _callback(msg):
@@ -194,6 +196,8 @@ def _callback(msg):
print "%s: 1 item %s" % (queue, count_str)
g.reset_caches()
+ c.use_write_db = {}
+
ret = callback(msg)
msg.channel.basic_ack(msg.delivery_tag)
sys.stdout.flush()
@@ -217,6 +221,7 @@ def handle_items(queue, callback, ack = True, limit = 1, drain = False,
"""Call callback() on every item in a particular queue. If the
connection to the queue is lost, it will die. Intended to be
used as a long-running process."""
+ from pylons import c
chan = connection_manager.get_channel()
countdown = None
@@ -238,6 +243,7 @@ def handle_items(queue, callback, ack = True, limit = 1, drain = False,
countdown = 1 + msg.delivery_info['message_count']
g.reset_caches()
+ c.use_write_db = {}
items = []
diff --git a/r2/r2/lib/app_globals.py b/r2/r2/lib/app_globals.py
index 8278fa1d9f..723e085de8 100644
--- a/r2/r2/lib/app_globals.py
+++ b/r2/r2/lib/app_globals.py
@@ -24,11 +24,11 @@
import pytz, os, logging, sys, socket, re, subprocess, random
import signal
from datetime import timedelta, datetime
-import pycassa
+from pycassa.pool import ConnectionPool as PycassaConnectionPool
from r2.lib.cache import LocalCache, SelfEmptyingCache
-from r2.lib.cache import CMemcache
+from r2.lib.cache import CMemcache, StaleCacheChain
from r2.lib.cache import HardCache, MemcacheChain, MemcacheChain, HardcacheChain
-from r2.lib.cache import CassandraCache, CassandraCacheChain, CacheChain, CL_ONE, CL_QUORUM, CL_ZERO
+from r2.lib.cache import CassandraCache, CassandraCacheChain, CacheChain, CL_ONE, CL_QUORUM
from r2.lib.utils import thread_dump
from r2.lib.db.stats import QueryStats
from r2.lib.translation import get_active_langs
@@ -37,7 +37,9 @@
class Globals(object):
- int_props = ['page_cache_time',
+ int_props = ['db_pool_size',
+ 'db_pool_overflow_size',
+ 'page_cache_time',
'solr_cache_time',
'num_mc_clients',
'MIN_DOWN_LINK',
@@ -81,12 +83,13 @@ class Globals(object):
'exception_logging',
'amqp_logging',
'read_only_mode',
+ 'frontpage_dart',
]
- tuple_props = ['memcaches',
+ tuple_props = ['stalecaches',
+ 'memcaches',
'permacache_memcaches',
'rendercaches',
- 'local_rendercache',
'servicecaches',
'cassandra_seeds',
'admins',
@@ -97,13 +100,12 @@ class Globals(object):
'allowed_css_linked_domains',
'authorized_cnames',
'hardcache_categories',
- 'proxy_addr']
+ 'proxy_addr',
+ 'allowed_pay_countries']
- choice_props = {'cassandra_rcl': {'ZERO': CL_ZERO,
- 'ONE': CL_ONE,
+ choice_props = {'cassandra_rcl': {'ONE': CL_ONE,
'QUORUM': CL_QUORUM},
- 'cassandra_wcl': {'ZERO': CL_ZERO,
- 'ONE': CL_ONE,
+ 'cassandra_wcl': {'ONE': CL_ONE,
'QUORUM': CL_QUORUM},
}
@@ -173,34 +175,33 @@ def __init__(self, global_conf, app_conf, paths, **extra):
if not self.cassandra_seeds:
raise ValueError("cassandra_seeds not set in the .ini")
- self.cassandra_seeds = list(self.cassandra_seeds)
- random.shuffle(self.cassandra_seeds)
- self.cassandra = pycassa.connect_thread_local(self.cassandra_seeds)
+ self.cassandra = PycassaConnectionPool('reddit',
+ server_list = self.cassandra_seeds,
+ # TODO: .ini setting
+ timeout=15, max_retries=3,
+ prefill=False)
perma_memcache = (CMemcache(self.permacache_memcaches, num_clients = num_mc_clients)
if self.permacache_memcaches
else None)
- self.permacache = self.init_cass_cache('permacache', 'permacache',
- self.cassandra,
- self.make_lock,
- memcache = perma_memcache,
- read_consistency_level = self.cassandra_rcl,
- write_consistency_level = self.cassandra_wcl,
- localcache_cls = localcache_cls)
+ self.permacache = CassandraCacheChain(localcache_cls(),
+ CassandraCache('permacache',
+ self.cassandra,
+ read_consistency_level = self.cassandra_rcl,
+ write_consistency_level = self.cassandra_wcl),
+ memcache = perma_memcache,
+ lock_factory = self.make_lock)
+
self.cache_chains.append(self.permacache)
- self.urlcache = self.init_cass_cache('permacache', 'urls',
- self.cassandra,
- self.make_lock,
- # TODO: increase this to QUORUM
- # once we switch to live
- read_consistency_level = self.cassandra_rcl,
- write_consistency_level = CL_ONE,
- localcache_cls = localcache_cls)
- self.cache_chains.append(self.urlcache)
# hardcache is done after the db info is loaded, and then the
# chains are reset to use the appropriate initial entries
- self.cache = MemcacheChain((localcache_cls(), self.memcache))
+ if self.stalecaches:
+ self.cache = StaleCacheChain(localcache_cls(),
+ CMemcache(self.stalecaches, num_clients=num_mc_clients),
+ self.memcache)
+ else:
+ self.cache = MemcacheChain((localcache_cls(), self.memcache))
self.cache_chains.append(self.cache)
self.rendercache = MemcacheChain((localcache_cls(),
@@ -327,6 +328,13 @@ def reset_caches():
self.reddit_host = socket.gethostname()
self.reddit_pid = os.getpid()
+ for arg in sys.argv:
+ tokens = arg.split("=")
+ if len(tokens) == 2:
+ k, v = tokens
+ self.log.debug("Overriding g.%s to %s" % (k, v))
+ setattr(self, k, v)
+
#the shutdown toggle
self.shutdown = False
@@ -357,20 +365,6 @@ def reset_caches():
(self.reddit_host, self.reddit_pid,
self.short_version, datetime.now()))
- def init_cass_cache(self, keyspace, column_family, cassandra_client,
- lock_factory,
- memcache = None,
- read_consistency_level = CL_ONE,
- write_consistency_level = CL_ONE,
- localcache_cls = LocalCache):
- return CassandraCacheChain(localcache_cls(),
- CassandraCache(keyspace, column_family,
- cassandra_client,
- read_consistency_level = read_consistency_level,
- write_consistency_level = write_consistency_level),
- memcache = memcache,
- lock_factory = lock_factory)
-
@staticmethod
def to_bool(x):
return (x.lower() == 'true') if x else None
@@ -388,7 +382,7 @@ def load_db_params(self, gc):
return
dbm = db_manager.db_manager()
- db_param_names = ('name', 'db_host', 'db_user', 'db_pass',
+ db_param_names = ('name', 'db_host', 'db_user', 'db_pass', 'db_port',
'pool_size', 'max_overflow')
for db_name in self.databases:
conf_params = self.to_iter(gc[db_name + '_db'])
@@ -397,6 +391,14 @@ def load_db_params(self, gc):
params['db_user'] = self.db_user
if params['db_pass'] == "*":
params['db_pass'] = self.db_pass
+ if params['db_port'] == "*":
+ params['db_port'] = self.db_port
+
+ if params['pool_size'] == "*":
+ params['pool_size'] = self.db_pool_size
+ if params['max_overflow'] == "*":
+ params['max_overflow'] = self.db_pool_overflow_size
+
ip = params['db_host']
ip_loads = get_db_load(self.servicecache, ip)
if ip not in ip_loads or ip_loads[ip][0] < 1000:
diff --git a/r2/r2/lib/authorize/api.py b/r2/r2/lib/authorize/api.py
index f871d10c27..ba2fe27349 100644
--- a/r2/r2/lib/authorize/api.py
+++ b/r2/r2/lib/authorize/api.py
@@ -227,7 +227,8 @@ def is_error_code(self, res, code):
def process_error(self, res):
- raise AuthorizeNetException, res
+ msg = "Response %r from request %r" % (res, self.toXML())
+ raise AuthorizeNetException(msg)
_autoclose_re = re.compile("<([^/]+)/>")
def _autoclose_handler(self, m):
diff --git a/r2/r2/lib/base.py b/r2/r2/lib/base.py
index fd41c1dc5f..70b8784eca 100644
--- a/r2/r2/lib/base.py
+++ b/r2/r2/lib/base.py
@@ -30,7 +30,7 @@
from utils import storify, string2js, read_http_date
from r2.lib.log import log_exception
-import re, md5
+import re, hashlib
from urllib import quote
import urllib2
import sys
@@ -61,7 +61,7 @@ def __call__(self, environ, start_response):
if (g.ip_hash
and true_client_ip
and ip_hash
- and md5.new(true_client_ip + g.ip_hash).hexdigest() \
+ and hashlib.md5(true_client_ip + g.ip_hash).hexdigest() \
== ip_hash.lower()):
request.ip = true_client_ip
elif remote_addr in g.proxy_addr and forwarded_for:
@@ -133,17 +133,20 @@ def format_output_url(cls, url, **kw):
# make sure to pass the port along if not 80
if not kw.has_key('port'):
kw['port'] = request.port
-
+
# disentagle the cname (for urls that would have
# cnameframe=1 in them)
u.mk_cname(**kw)
-
+
# make sure the extensions agree with the current page
if c.extension:
u.set_extension(c.extension)
# unparse and encode it un utf8
- return _force_unicode(u.unparse()).encode('utf8')
+ rv = _force_unicode(u.unparse()).encode('utf8')
+ if any(ch.isspace() for ch in rv):
+ raise ValueError("Space characters in redirect URL: [%r]" % rv)
+ return rv
@classmethod
@@ -158,11 +161,11 @@ def intermediate_redirect(cls, form_path):
params = dict(dest = cls.format_output_url(request.fullpath))
if c.extension == "widget" and request.GET.get("callback"):
params['callback'] = request.GET.get("callback")
-
+
path = add_sr(cls.format_output_url(form_path) +
query_string(params))
return cls.redirect(path)
-
+
@classmethod
def redirect(cls, dest, code = 302):
"""
diff --git a/r2/r2/lib/cache.py b/r2/r2/lib/cache.py
index 3068160c2a..a127fd365f 100644
--- a/r2/r2/lib/cache.py
+++ b/r2/r2/lib/cache.py
@@ -27,11 +27,12 @@
import pylibmc
from _pylibmc import MemcachedError
-import pycassa
-import cassandra.ttypes
+from pycassa import ColumnFamily
+from pycassa.cassandra.ttypes import ConsistencyLevel
+from pycassa.cassandra.ttypes import NotFoundException as CassandraNotFound
from r2.lib.contrib import memcache
-from r2.lib.utils import in_chunks, prefix_keys
+from r2.lib.utils import in_chunks, prefix_keys, trace
from r2.lib.hardcachebackend import HardCacheBackend
from r2.lib.sgm import sgm # get this into our namespace so that it's
@@ -51,8 +52,8 @@ def add_multi(self, keys, prefix='', time=0):
for k,v in keys.iteritems():
self.add(prefix+str(k), v, time = time)
- def get_multi(self, keys, prefix=''):
- return prefix_keys(keys, prefix, self.simple_get_multi)
+ def get_multi(self, keys, prefix='', **kw):
+ return prefix_keys(keys, prefix, lambda k: self.simple_get_multi(k, **kw))
class PyMemcache(CacheUtils, memcache.Client):
"""We still use our patched python-memcache to talk to the
@@ -382,11 +383,11 @@ def get(self, key, default = None, allow_local = True):
return default
- def get_multi(self, keys, prefix='', allow_local = True):
- l = lambda ks: self.simple_get_multi(ks, allow_local = allow_local)
+ def get_multi(self, keys, prefix='', allow_local = True, **kw):
+ l = lambda ks: self.simple_get_multi(ks, allow_local = allow_local, **kw)
return prefix_keys(keys, prefix, l)
- def simple_get_multi(self, keys, allow_local = True):
+ def simple_get_multi(self, keys, allow_local = True, stale=None):
out = {}
need = set(keys)
for c in self.caches:
@@ -462,15 +463,95 @@ def accrue(self, key, time=0, delta=1):
for c in self.caches:
c.set(key, auth_value, time=time)
+ return auth_value
+
@property
def backend(self):
# the hardcache is always the last item in a HardCacheChain
return self.caches[-1].backend
-CL_ZERO = cassandra.ttypes.ConsistencyLevel.ZERO
-CL_ONE = cassandra.ttypes.ConsistencyLevel.ONE
-CL_QUORUM = cassandra.ttypes.ConsistencyLevel.QUORUM
-CL_ALL = cassandra.ttypes.ConsistencyLevel.ALL
+class StaleCacheChain(CacheChain):
+ """A cache chain of two cache chains. When allowed by `stale`,
+ answers may be returned by a "closer" but potentially older
+ cache. Probably doesn't play well with NoneResult cacheing"""
+ staleness = 30
+
+ def __init__(self, localcache, stalecache, realcache):
+ self.localcache = localcache
+ self.stalecache = stalecache
+ self.realcache = realcache
+ self.caches = (localcache, realcache) # for the other
+ # CacheChain machinery
+
+ def get(self, key, default=None, stale = False, **kw):
+ if kw.get('allow_local', True) and key in self.caches[0]:
+ return self.caches[0][key]
+
+ if stale:
+ stale_value = self._getstale([key]).get(key, None)
+ if stale_value is not None:
+ return stale_value # never return stale data into the
+ # LocalCache, or people that didn't
+ # say they'll take stale data may
+ # get it
+
+ value = CacheChain.get(self, key, **kw)
+ if value is None:
+ return default
+
+ if value is not None and stale:
+ self.stalecache.set(key, value, time=self.staleness)
+
+ return value
+
+ def simple_get_multi(self, keys, stale = False, **kw):
+ if not isinstance(keys, set):
+ keys = set(keys)
+
+ ret = {}
+
+ if kw.get('allow_local'):
+ for k in list(keys):
+ if k in self.localcache:
+ ret[k] = self.localcache[k]
+ keys.remove(k)
+
+ if keys and stale:
+ stale_values = self._getstale(keys)
+ # never put stale data into the localcache
+ for k, v in stale_values.iteritems():
+ ret[k] = v
+ keys.remove(k)
+
+ if keys:
+ values = self.realcache.simple_get_multi(keys)
+ if values and stale:
+ self.stalecache.set_multi(values, time=self.staleness)
+ self.localcache.update(values)
+ ret.update(values)
+
+ return ret
+
+ def _getstale(self, keys):
+ # this is only in its own function to make tapping it for
+ # debugging easier
+ return self.stalecache.simple_get_multi(keys)
+
+ def reset(self):
+ newcache = self.localcache.__class__()
+ self.localcache = newcache
+ self.caches = (newcache,) + self.caches[1:]
+ if isinstance(self.realcache, CacheChain):
+ assert isinstance(self.realcache.caches[0], LocalCache)
+ self.realcache.caches = (newcache,) + self.realcache.caches[1:]
+
+ def __repr__(self):
+ return '<%s %r>' % (self.__class__.__name__,
+ (self.localcache, self.stalecache, self.realcache))
+
+CL_ONE = ConsistencyLevel.ONE
+CL_QUORUM = ConsistencyLevel.QUORUM
+CL_ALL = ConsistencyLevel.ALL
class CassandraCacheChain(CacheChain):
def __init__(self, localcache, cassa, lock_factory, memcache=None, **kw):
@@ -484,7 +565,7 @@ def __init__(self, localcache, cassa, lock_factory, memcache=None, **kw):
self.make_lock = lock_factory
CacheChain.__init__(self, caches, **kw)
- def mutate(self, key, mutation_fn, default = None):
+ def mutate(self, key, mutation_fn, default = None, willread=True):
"""Mutate a Cassandra key as atomically as possible"""
with self.make_lock('mutate_%s' % key):
# we have to do some of the the work of the cache chain
@@ -500,28 +581,29 @@ def mutate(self, key, mutation_fn, default = None):
# which would require some more row-cache performace
# testing)
rcl = wcl = self.cassa.write_consistency_level
- if rcl == CL_ZERO:
- rcl = CL_ONE
- try:
+ if willread:
+ try:
+ value = None
+ if self.memcache:
+ value = self.memcache.get(key)
+ if value is None:
+ value = self.cassa.get(key,
+ read_consistency_level = rcl)
+ except cassandra.ttypes.NotFoundException:
+ value = default
+
+ # due to an old bug in NoneResult caching, we still
+ # have some of these around
+ if value == NoneResult:
+ value = default
+
+ else:
value = None
- if self.memcache:
- value = self.memcache.get(key)
- if value is None:
- value = self.cassa.get(key,
- read_consistency_level = rcl)
- except cassandra.ttypes.NotFoundException:
- value = default
-
- # due to an old bug in NoneResult caching, we still have
- # some of these around
- if value == NoneResult:
- value = default
-
- new_value = mutation_fn(copy(value)) # send in a copy in
- # case they mutate it
- # in-place
-
- if value != new_value:
+
+ # send in a copy in case they mutate it in-place
+ new_value = mutation_fn(copy(value))
+
+ if not willread or value != new_value:
self.cassa.set(key, new_value,
write_consistency_level = wcl)
for ca in self.caches[:-1]:
@@ -549,20 +631,19 @@ def bulk_load(self, start='', end='', chunk_size = 100):
class CassandraCache(CacheUtils):
- """A cache that uses a Cassandra cluster. Uses a single keyspace
- and column family and only the column-name 'value'"""
- def __init__(self, keyspace, column_family, client,
+ """A cache that uses a Cassandra ColumnFamily. Uses only the
+ column-name 'value'"""
+ def __init__(self, column_family, client,
read_consistency_level = CL_ONE,
write_consistency_level = CL_QUORUM):
- self.keyspace = keyspace
self.column_family = column_family
self.client = client
self.read_consistency_level = read_consistency_level
self.write_consistency_level = write_consistency_level
- self.cf = pycassa.ColumnFamily(self.client, self.keyspace,
- self.column_family,
- read_consistency_level = read_consistency_level,
- write_consistency_level = write_consistency_level)
+ self.cf = ColumnFamily(self.client,
+ self.column_family,
+ read_consistency_level = read_consistency_level,
+ write_consistency_level = write_consistency_level)
def _rcl(self, alternative):
return (alternative if alternative is not None
@@ -578,7 +659,7 @@ def get(self, key, default = None, read_consistency_level = None):
row = self.cf.get(key, columns=['value'],
read_consistency_level = rcl)
return pickle.loads(row['value'])
- except (cassandra.ttypes.NotFoundException, KeyError):
+ except (CassandraNotFound, KeyError):
return default
def simple_get_multi(self, keys, read_consistency_level = None):
@@ -590,29 +671,36 @@ def simple_get_multi(self, keys, read_consistency_level = None):
for (key, row) in rows.iteritems())
def set(self, key, val,
- write_consistency_level = None, time = None):
+ write_consistency_level = None,
+ time = None):
if val == NoneResult:
# NoneResult caching is for other parts of the chain
return
wcl = self._wcl(write_consistency_level)
ret = self.cf.insert(key, {'value': pickle.dumps(val)},
- write_consistency_level = wcl)
+ write_consistency_level = wcl,
+ ttl = time)
self._warm([key])
return ret
def set_multi(self, keys, prefix='',
- write_consistency_level = None, time = None):
+ write_consistency_level = None,
+ time = None):
if not isinstance(keys, dict):
+ # allow iterables yielding tuples
keys = dict(keys)
- keys = dict(('%s%s' % (prefix, key), val)
- for (key, val) in keys.iteritems())
+
wcl = self._wcl(write_consistency_level)
ret = {}
- for key, val in keys.iteritems():
- if val != NoneResult:
- ret[key] = self.cf.insert(key, {'value': pickle.dumps(val)},
- write_consistency_level = wcl)
+
+ with self.cf.batch(write_consistency_level = wcl):
+ for key, val in keys.iteritems():
+ if val != NoneResult:
+ ret[key] = self.cf.insert('%s%s' % (prefix, key),
+ {'value': pickle.dumps(val)},
+ ttl = time)
+
self._warm(keys.keys())
return ret
@@ -739,3 +827,25 @@ def _conv(s):
h.update(_conv(kw))
return '%s(%s)' % (iden, h.hexdigest())
+
+def test_stale():
+ from pylons import g
+ ca = g.cache
+ assert isinstance(ca, StaleCacheChain)
+
+ ca.localcache.clear()
+
+ ca.stalecache.set('foo', 'bar', time=ca.staleness)
+ assert ca.stalecache.get('foo') == 'bar'
+ ca.realcache.set('foo', 'baz')
+ assert ca.realcache.get('foo') == 'baz'
+
+ assert ca.get('foo', stale=True) == 'bar'
+ ca.localcache.clear()
+ assert ca.get('foo', stale=False) == 'baz'
+ ca.localcache.clear()
+
+ assert ca.get_multi(['foo'], stale=True) == {'foo': 'bar'}
+ assert len(ca.localcache) == 0
+ assert ca.get_multi(['foo'], stale=False) == {'foo': 'baz'}
+ ca.localcache.clear()
diff --git a/r2/r2/lib/comment_tree.py b/r2/r2/lib/comment_tree.py
index 98f3c6d181..f1e20d16e8 100644
--- a/r2/r2/lib/comment_tree.py
+++ b/r2/r2/lib/comment_tree.py
@@ -25,6 +25,8 @@
from r2.lib.db.sorts import epoch_seconds
from r2.lib.cache import sgm
+MAX_ITERATIONS = 20000
+
def comments_key(link_id):
return 'comments_' + str(link_id)
@@ -308,10 +310,14 @@ def _load_link_comments(link_id):
for cm_id in cids:
num = 0
todo = [cm_id]
+ iteration_count = 0
while todo:
+ if iteration_count > MAX_ITERATIONS:
+ raise Exception("bad comment tree for link %s" % link_id)
more = comment_tree.get(todo.pop(0), ())
num += len(more)
todo.extend(more)
+ iteration_count += 1
num_children[cm_id] = num
return cids, comment_tree, depth, num_children
diff --git a/r2/r2/lib/contrib/discount/Makefile b/r2/r2/lib/contrib/discount/Makefile
index 1d81e559a3..923302b348 100644
--- a/r2/r2/lib/contrib/discount/Makefile
+++ b/r2/r2/lib/contrib/discount/Makefile
@@ -13,7 +13,8 @@ SAMPLE_PGMS+= theme
MKDLIB=libmarkdown.a
OBJS=mkdio.o markdown.o dumptree.o generate.o \
resource.o docheader.o version.o toc.o css.o \
- xml.o Csio.o xmlpage.o basename.o emmatch.o
+ xml.o Csio.o xmlpage.o basename.o emmatch.o \
+ tags.o html5.o
MAN3PAGES=mkd-callbacks.3 mkd-functions.3 markdown.3 mkd-line.3
@@ -28,11 +29,11 @@ install.everything: install install.samples install.man
install.samples: $(SAMPLE_PGMS) install
/usr/bin/install -s -m 755 $(SAMPLE_PGMS) $(DESTDIR)/$(BINDIR)
- /home/raldi/reddit/r2/r2/lib/contrib/discount/config.md $(DESTDIR)/$(MANDIR)/man1
+ /tmp/discount-1.6.8/config.md $(DESTDIR)/$(MANDIR)/man1
/usr/bin/install -m 444 theme.1 $(DESTDIR)/$(MANDIR)/man1
install.man:
- /home/raldi/reddit/r2/r2/lib/contrib/discount/config.md $(DESTDIR)/$(MANDIR)/man3
+ /tmp/discount-1.6.8/config.md $(DESTDIR)/$(MANDIR)/man3
/usr/bin/install -m 444 $(MAN3PAGES) $(DESTDIR)/$(MANDIR)/man3
for x in mkd_line mkd_generateline; do \
( echo '.\"' ; echo ".so man3/mkd-line.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3;\
@@ -43,9 +44,9 @@ install.man:
for x in mkd_compile mkd_css mkd_generatecss mkd_generatehtml mkd_cleanup mkd_doc_title mkd_doc_author mkd_doc_date; do \
( echo '.\"' ; echo ".so man3/mkd-functions.3" ) > $(DESTDIR)/$(MANDIR)/man3/$$x.3; \
done
- /home/raldi/reddit/r2/r2/lib/contrib/discount/config.md $(DESTDIR)/$(MANDIR)/man7
+ /tmp/discount-1.6.8/config.md $(DESTDIR)/$(MANDIR)/man7
/usr/bin/install -m 444 markdown.7 mkd-extensions.7 $(DESTDIR)/$(MANDIR)/man7
- /home/raldi/reddit/r2/r2/lib/contrib/discount/config.md $(DESTDIR)/$(MANDIR)/man1
+ /tmp/discount-1.6.8/config.md $(DESTDIR)/$(MANDIR)/man1
/usr/bin/install -m 444 markdown.1 $(DESTDIR)/$(MANDIR)/man1
install.everything: install install.man
@@ -79,9 +80,9 @@ test: $(PGMS) echo cols
sh $$x || exit 1; \
done
-cols: tools/cols.c
+cols: tools/cols.c config.h
$(CC) -o cols tools/cols.c
-echo: tools/echo.c
+echo: tools/echo.c config.h
$(CC) -o echo tools/echo.c
clean:
diff --git a/r2/r2/lib/contrib/discount/Makefile.in b/r2/r2/lib/contrib/discount/Makefile.in
index c4d5d562fd..44a5895dbd 100644
--- a/r2/r2/lib/contrib/discount/Makefile.in
+++ b/r2/r2/lib/contrib/discount/Makefile.in
@@ -13,7 +13,8 @@ SAMPLE_PGMS=mkd2html makepage
MKDLIB=libmarkdown.a
OBJS=mkdio.o markdown.o dumptree.o generate.o \
resource.o docheader.o version.o toc.o css.o \
- xml.o Csio.o xmlpage.o basename.o emmatch.o @AMALLOC@
+ xml.o Csio.o xmlpage.o basename.o emmatch.o \
+ tags.o html5.o @AMALLOC@
MAN3PAGES=mkd-callbacks.3 mkd-functions.3 markdown.3 mkd-line.3
@@ -79,9 +80,9 @@ test: $(PGMS) echo cols
sh $$x || exit 1; \
done
-cols: tools/cols.c
+cols: tools/cols.c config.h
$(CC) -o cols tools/cols.c
-echo: tools/echo.c
+echo: tools/echo.c config.h
$(CC) -o echo tools/echo.c
clean:
diff --git a/r2/r2/lib/contrib/discount/Plan9/mkfile b/r2/r2/lib/contrib/discount/Plan9/mkfile
index 189d7e9287..f15f987836 100644
--- a/r2/r2/lib/contrib/discount/Plan9/mkfile
+++ b/r2/r2/lib/contrib/discount/Plan9/mkfile
@@ -1,5 +1,5 @@
BIN=/$objtype/bin
-CC='cc -D_BSD_EXTENSION'
+CC='cc -D_BSD_EXTENSION -D_C99_SNPRINTF_EXTENSION'
markdown:
ape/psh -c 'cd .. && make'
diff --git a/r2/r2/lib/contrib/discount/VERSION b/r2/r2/lib/contrib/discount/VERSION
index 9edc58bb1d..d8c5e721a7 100644
--- a/r2/r2/lib/contrib/discount/VERSION
+++ b/r2/r2/lib/contrib/discount/VERSION
@@ -1 +1 @@
-1.6.4
+1.6.8
diff --git a/r2/r2/lib/contrib/discount/config.cmd b/r2/r2/lib/contrib/discount/config.cmd
index ed15bbbbda..59920908c2 100755
--- a/r2/r2/lib/contrib/discount/config.cmd
+++ b/r2/r2/lib/contrib/discount/config.cmd
@@ -1,2 +1,2 @@
#! /bin/sh
- ./configure.sh
+ ./configure.sh '--relaxed-emphasis' '--enable-superscript'
diff --git a/r2/r2/lib/contrib/discount/config.h b/r2/r2/lib/contrib/discount/config.h
index ec9f2e6ebd..80ccf973f9 100644
--- a/r2/r2/lib/contrib/discount/config.h
+++ b/r2/r2/lib/contrib/discount/config.h
@@ -1,5 +1,5 @@
/*
- * configuration for markdown, generated Mon May 17 13:43:31 PDT 2010
+ * configuration for markdown, generated Mon Oct 18 16:39:05 PDT 2010
* by raldi@zork
*/
#ifndef __AC_MARKDOWN_D
@@ -20,6 +20,8 @@
#define HAVE_STRNCASECMP 1
#define HAVE_FCHDIR 1
#define TABSTOP 4
+#define SUPERSCRIPT 1
+#define RELAXED_EMPHASIS 1
#define HAVE_MALLOC_H 1
#define PATH_SED "/bin/sed"
diff --git a/r2/r2/lib/contrib/discount/config.log b/r2/r2/lib/contrib/discount/config.log
index 1231807308..5b1c680093 100644
--- a/r2/r2/lib/contrib/discount/config.log
+++ b/r2/r2/lib/contrib/discount/config.log
@@ -8,19 +8,19 @@ checking out the C compiler
checking for "volatile" keyword
checking for "const" keyword
defining WORD & DWORD scalar types
-/tmp/pd26167.c: In function 'main':
-/tmp/pd26167.c:29: warning: incompatible implicit declaration of built-in function 'exit'
-/tmp/pd26167.c:16: warning: return type of 'main' is not 'int'
-/tmp/ngc26167.c: In function 'main':
-/tmp/ngc26167.c:5: warning: initialization makes pointer from integer without a cast
-/tmp/ngc26167.c:6: warning: initialization makes pointer from integer without a cast
+/tmp/pd16169.c: In function 'main':
+/tmp/pd16169.c:29: warning: incompatible implicit declaration of built-in function 'exit'
+/tmp/pd16169.c:16: warning: return type of 'main' is not 'int'
+/tmp/ngc16169.c: In function 'main':
+/tmp/ngc16169.c:5: warning: initialization makes pointer from integer without a cast
+/tmp/ngc16169.c:6: warning: initialization makes pointer from integer without a cast
looking for header libgen.h
looking for header pwd.h
looking for the getpwuid function
looking for the srandom function
looking for the bzero function
-/tmp/ngc26167.c: In function 'main':
-/tmp/ngc26167.c:4: warning: incompatible implicit declaration of built-in function 'bzero'
+/tmp/ngc16169.c: In function 'main':
+/tmp/ngc16169.c:4: warning: incompatible implicit declaration of built-in function 'bzero'
looking for the random function
looking for the strcasecmp function
looking for the strncasecmp function
diff --git a/r2/r2/lib/contrib/discount/config.md b/r2/r2/lib/contrib/discount/config.md
index 7e9437fb90..856b259597 100755
--- a/r2/r2/lib/contrib/discount/config.md
+++ b/r2/r2/lib/contrib/discount/config.md
@@ -1,5 +1,5 @@
#! /bin/sh
-# script generated Mon May 17 13:43:31 PDT 2010 by configure.sh
+# script generated Mon Oct 18 16:39:05 PDT 2010 by configure.sh
test -d "$1" || mkdir -p "$1"
exit 0
diff --git a/r2/r2/lib/contrib/discount/config.sub b/r2/r2/lib/contrib/discount/config.sub
index 6fb1d363ea..39e1e4e639 100644
--- a/r2/r2/lib/contrib/discount/config.sub
+++ b/r2/r2/lib/contrib/discount/config.sub
@@ -3,7 +3,7 @@ s;@CPPFLAGS@;;g
s;@INSTALL@;/usr/bin/install;g
s;@INSTALL_PROGRAM@;/usr/bin/install -s -m 755;g
s;@INSTALL_DATA@;/usr/bin/install -m 444;g
-s;@INSTALL_DIR@;/home/raldi/reddit/r2/r2/lib/contrib/discount/config.md;g
+s;@INSTALL_DIR@;/tmp/discount-1.6.8/config.md;g
s;@CC@;cc;g
s;@AR@;/usr/bin/ar;g
s;@RANLIB@;/usr/bin/ranlib;g
@@ -13,13 +13,13 @@ s:@BYTE@:unsigned char:g
s;@THEME@;;g
s;@TABSTOP@;4;g
s;@AMALLOC@;;g
-s;@STRICT@;.\";g
+s;@STRICT@;;g
s;@LIBS@;;g
s;@CONFIGURE_FILES@;config.cmd config.sub config.h config.mak config.log config.md;g
s;@GENERATED_FILES@;Makefile version.c markdown.1;g
s;@CFLAGS@;-g;g
s;@LDFLAGS@;-g;g
-s;@srcdir@;/home/raldi/reddit/r2/r2/lib/contrib/discount;g
+s;@srcdir@;/tmp/discount-1.6.8;g
s;@prefix@;/usr/local;g
s;@exedir@;/usr/local/bin;g
s;@sbindir@;/usr/local/sbin;g
diff --git a/r2/r2/lib/contrib/discount/cstring.h b/r2/r2/lib/contrib/discount/cstring.h
index 164e75bb67..86755e8a9e 100644
--- a/r2/r2/lib/contrib/discount/cstring.h
+++ b/r2/r2/lib/contrib/discount/cstring.h
@@ -10,7 +10,9 @@
#include
#include
-#include "amalloc.h"
+#ifndef __WITHOUT_AMALLOC
+# include "amalloc.h"
+#endif
/* expandable Pascal-style string.
*/
diff --git a/r2/r2/lib/contrib/discount/dumptree.c b/r2/r2/lib/contrib/discount/dumptree.c
index ecace98aa0..068084880d 100644
--- a/r2/r2/lib/contrib/discount/dumptree.c
+++ b/r2/r2/lib/contrib/discount/dumptree.c
@@ -33,6 +33,7 @@ Pptype(int typ)
case HR : return "hr";
case TABLE : return "table";
case SOURCE : return "source";
+ case STYLE : return "style";
default : return "mystery node!";
}
}
diff --git a/r2/r2/lib/contrib/discount/generate.c b/r2/r2/lib/contrib/discount/generate.c
index 075a203bb0..7fe12e9861 100644
--- a/r2/r2/lib/contrib/discount/generate.c
+++ b/r2/r2/lib/contrib/discount/generate.c
@@ -18,7 +18,7 @@
#include "amalloc.h"
typedef int (*stfu)(const void*,const void*);
-
+typedef void (*spanhandler)(MMIOT*,int);
/* forward declarations */
static void text(MMIOT *f);
@@ -164,16 +164,6 @@ Qprintf(MMIOT *f, char *fmt, ...)
}
-/* Qcopy()
- */
-static void
-Qcopy(int count, MMIOT *f)
-{
- while ( count-- > 0 )
- Qchar(pull(f), f);
-}
-
-
/* Qem()
*/
static void
@@ -272,12 +262,12 @@ parenthetical(int in, int out, MMIOT *f)
for ( indent=1,size=0; indent; size++ ) {
if ( (c = pull(f)) == EOF )
return EOF;
- else if ( c == in )
- ++indent;
- else if ( (c == '\\') && (peek(f,1) == out) ) {
+ else if ( (c == '\\') && (peek(f,1) == out || peek(f,1) == in) ) {
++size;
pull(f);
}
+ else if ( c == in )
+ ++indent;
else if ( c == out )
--indent;
}
@@ -664,11 +654,11 @@ mangle(char *s, int len, MMIOT *f)
/* nrticks() -- count up a row of tick marks
*/
static int
-nrticks(int offset, MMIOT *f)
+nrticks(int offset, int tickchar, MMIOT *f)
{
int tick = 0;
- while ( peek(f, offset+tick) == '`' ) tick++;
+ while ( peek(f, offset+tick) == tickchar ) tick++;
return tick;
} /* nrticks */
@@ -677,36 +667,34 @@ nrticks(int offset, MMIOT *f)
/* matchticks() -- match a certain # of ticks, and if that fails
* match the largest subset of those ticks.
*
- * if a subset was matched, modify the passed in
- * # of ticks so that the caller (text()) can
- * appropriately process the horrible thing.
+ * if a subset was matched, return the # of ticks
+ * that were matched.
*/
static int
-matchticks(MMIOT *f, int *ticks)
+matchticks(MMIOT *f, int tickchar, int ticks, int *endticks)
{
- int size, tick, c;
+ int size, count, c;
int subsize=0, subtick=0;
- for (size = *ticks; (c=peek(f,size)) != EOF; ) {
- if ( c == '`' )
- if ( (tick=nrticks(size,f)) == *ticks )
+ *endticks = ticks;
+ for (size = 0; (c=peek(f,size+ticks)) != EOF; size ++) {
+ if ( (c == tickchar) && ( count = nrticks(size+ticks,tickchar,f)) ) {
+ if ( count == ticks )
return size;
- else {
- if ( tick > subtick ) {
+ else if ( count ) {
+ if ( (count > subtick) && (count < ticks) ) {
subsize = size;
- subtick = tick;
+ subtick = count;
}
- size += tick;
+ size += count;
}
- else
- size++;
+ }
}
if ( subsize ) {
- *ticks = subtick;
+ *endticks = subtick;
return subsize;
}
return 0;
-
} /* matchticks */
@@ -727,13 +715,24 @@ code(MMIOT *f, char *s, int length)
} /* code */
+/* delspan() -- write out a chunk of text, blocking with ...
+ */
+static void
+delspan(MMIOT *f, int size)
+{
+ Qstring("", f);
+ ___mkd_reparse(cursor(f)-1, size, 0, f);
+ Qstring("", f);
+}
+
+
/* codespan() -- write out a chunk of text as code, trimming one
* space off the front and/or back as appropriate.
*/
static void
codespan(MMIOT *f, int size)
{
- int i=0, c;
+ int i=0;
if ( size > 1 && peek(f, size-1) == ' ' ) --size;
if ( peek(f,i) == ' ' ) ++i, --size;
@@ -1058,6 +1057,30 @@ smartypants(int c, int *flags, MMIOT *f)
} /* smartypants */
+/* process a body of text encased in some sort of tick marks. If it
+ * works, generate the output and return 1, otherwise just return 0 and
+ * let the caller figure it out.
+ */
+static int
+tickhandler(MMIOT *f, int tickchar, int minticks, spanhandler spanner)
+{
+ int endticks, size;
+ int tick = nrticks(0, tickchar, f);
+
+ if ( (tick >= minticks) && (size = matchticks(f,tickchar,tick,&endticks)) ) {
+ if ( endticks < tick ) {
+ size += (tick - endticks);
+ tick = endticks;
+ }
+
+ shift(f, tick);
+ (*spanner)(f,size);
+ shift(f, size+tick-1);
+ return 1;
+ }
+ return 0;
+}
+
#define tag_text(f) (f->flags & INSIDE_TAG)
@@ -1151,21 +1174,12 @@ text(MMIOT *f)
}
break;
- case '`': if ( tag_text(f) )
+ case '~': if ( (f->flags & (NOSTRIKETHROUGH|INSIDE_TAG|STRICT)) || !tickhandler(f,c,2,delspan) )
Qchar(c, f);
- else {
- int size, tick = nrticks(0, f);
+ break;
- if ( size = matchticks(f, &tick) ) {
- shift(f, tick);
- codespan(f, size-tick);
- shift(f, size-1);
- }
- else {
- Qchar(c, f);
- Qcopy(tick-1, f);
- }
- }
+ case '`': if ( tag_text(f) || !tickhandler(f,c,1,codespan) )
+ Qchar(c, f);
break;
case '\\': switch ( c = pull(f) ) {
@@ -1333,8 +1347,8 @@ static int
printblock(Paragraph *pp, MMIOT *f)
{
Line *t = pp->text;
- static char *Begin[] = { "", "
", "
" };
- static char *End[] = { "", "
","" };
+ static char *Begin[] = { "", "
", "
" };
+ static char *End[] = { "", "
","" };
while (t) {
if ( S(t->text) ) {
diff --git a/r2/r2/lib/contrib/discount/html5.c b/r2/r2/lib/contrib/discount/html5.c
new file mode 100644
index 0000000000..8b869885aa
--- /dev/null
+++ b/r2/r2/lib/contrib/discount/html5.c
@@ -0,0 +1,24 @@
+/* block-level tags for passing html5 blocks through the blender
+ */
+#include "tags.h"
+
+void
+mkd_with_html5_tags()
+{
+ static int populated = 0;
+
+ if ( populated ) return;
+ populated = 1;
+
+ mkd_prepare_tags();
+
+ mkd_define_tag("ASIDE", 0);
+ mkd_define_tag("FOOTER", 0);
+ mkd_define_tag("HEADER", 0);
+ mkd_define_tag("HGROUP", 0);
+ mkd_define_tag("NAV", 0);
+ mkd_define_tag("SECTION", 0);
+ mkd_define_tag("ARTICLE", 0);
+
+ mkd_sort_tags();
+}
diff --git a/r2/r2/lib/contrib/discount/main.c b/r2/r2/lib/contrib/discount/main.c
index 3d6e502603..fcde68a2fc 100644
--- a/r2/r2/lib/contrib/discount/main.c
+++ b/r2/r2/lib/contrib/discount/main.c
@@ -56,6 +56,8 @@ static struct {
{ "toc", 0, MKD_TOC },
{ "autolink",0, MKD_AUTOLINK },
{ "safelink",0, MKD_SAFELINK },
+ { "del", 1, MKD_NOSTRIKETHROUGH },
+ { "strikethrough", 1, MKD_NOSTRIKETHROUGH },
{ "1.0", 0, MKD_1_COMPAT },
} ;
@@ -113,6 +115,7 @@ main(int argc, char **argv)
int flags = 0;
int debug = 0;
int toc = 0;
+ int with_html5 = 0;
int use_mkd_line = 0;
char *urlflags = 0;
char *text = 0;
@@ -127,13 +130,16 @@ main(int argc, char **argv)
pgm = basename(argv[0]);
opterr = 1;
- while ( (opt=getopt(argc, argv, "b:df:E:F:o:s:t:TV")) != EOF ) {
+ while ( (opt=getopt(argc, argv, "5b:df:E:F:o:s:t:TV")) != EOF ) {
switch (opt) {
+ case '5': with_html5 = 1;
+ break;
case 'b': urlbase = optarg;
break;
case 'd': debug = 1;
break;
- case 'V': printf("%s: discount %s\n", pgm, markdown_version);
+ case 'V': printf("%s: discount %s%s\n", pgm, markdown_version,
+ with_html5 ? " +html5":"");
exit(0);
case 'E': urlflags = optarg;
break;
@@ -167,6 +173,9 @@ main(int argc, char **argv)
argc -= optind;
argv += optind;
+ if ( with_html5 )
+ mkd_with_html5_tags();
+
if ( use_mkd_line )
rc = mkd_generateline( text, strlen(text), stdout, flags);
else {
diff --git a/r2/r2/lib/contrib/discount/markdown.1 b/r2/r2/lib/contrib/discount/markdown.1
index 8f5ea3ee7d..af97649314 100644
--- a/r2/r2/lib/contrib/discount/markdown.1
+++ b/r2/r2/lib/contrib/discount/markdown.1
@@ -59,10 +59,10 @@ Do not process pandoc headers.
Do not process Markdown Extra-style tables.
.It Ar tabstops
Use markdown-standard 4-space tabstops.
-.".It Ar strict
-."Disable superscript and relaxed emphasis.
-.".It Ar relax
-."Enable superscript and relaxed emphasis (this is the default.)
+.It Ar strict
+Disable superscript and relaxed emphasis.
+.It Ar relax
+Enable superscript and relaxed emphasis (this is the default.)
.It Ar toc
Enable table-of-contents support
.It Ar 1.0
diff --git a/r2/r2/lib/contrib/discount/markdown.c b/r2/r2/lib/contrib/discount/markdown.c
index 95b402cd8b..dc7deea6d0 100644
--- a/r2/r2/lib/contrib/discount/markdown.c
+++ b/r2/r2/lib/contrib/discount/markdown.c
@@ -4,6 +4,8 @@
* The redistribution terms are provided in the COPYRIGHT file that must
* be distributed with this source code.
*/
+#include "config.h"
+
#include
#include
#include
@@ -11,51 +13,15 @@
#include
#include
-#include "config.h"
-
#include "cstring.h"
#include "markdown.h"
#include "amalloc.h"
-
-/* block-level tags for passing html blocks through the blender
- */
-struct kw {
- char *id;
- int size;
- int selfclose;
-} ;
-
-#define KW(x) { x, sizeof(x)-1, 0 }
-#define SC(x) { x, sizeof(x)-1, 1 }
-
-static struct kw blocktags[] = { KW("!--"), KW("STYLE"), KW("SCRIPT"),
- KW("ADDRESS"), KW("BDO"), KW("BLOCKQUOTE"),
- KW("CENTER"), KW("DFN"), KW("DIV"), KW("H1"),
- KW("H2"), KW("H3"), KW("H4"), KW("H5"),
- KW("H6"), KW("LISTING"), KW("NOBR"),
- KW("UL"), KW("P"), KW("OL"), KW("DL"),
- KW("PLAINTEXT"), KW("PRE"), KW("TABLE"),
- KW("WBR"), KW("XMP"), SC("HR"), SC("BR"),
- KW("IFRAME"), KW("MAP") };
-#define SZTAGS (sizeof blocktags / sizeof blocktags[0])
-#define MAXTAG 11 /* sizeof "BLOCKQUOTE" */
+#include "tags.h"
typedef int (*stfu)(const void*,const void*);
typedef ANCHOR(Paragraph) ParagraphRoot;
-
-/* case insensitive string sort (for qsort() and bsearch() of block tags)
- */
-static int
-casort(struct kw *a, struct kw *b)
-{
- if ( a->size != b->size )
- return a->size - b->size;
- return strncasecmp(a->id, b->id, b->size);
-}
-
-
/* case insensitive string sort for Footnote tags.
*/
int
@@ -135,19 +101,28 @@ ___mkd_tidy(Cstring *t)
}
+static struct kw comment = { "!--", 3, 0 };
+
static struct kw *
isopentag(Line *p)
{
int i=0, len;
- struct kw key, *ret;
+ char *line;
if ( !p ) return 0;
+ line = T(p->text);
len = S(p->text);
- if ( len < 3 || T(p->text)[0] != '<' )
+ if ( len < 3 || line[0] != '<' )
return 0;
+ if ( line[1] == '!' && line[2] == '-' && line[3] == '-' )
+ /* comments need special case handling, because
+ * the !-- doesn't need to end in a whitespace
+ */
+ return &comment;
+
/* find how long the tag is so we can check to see if
* it's a block-level tag
*/
@@ -156,13 +131,8 @@ isopentag(Line *p)
&& !isspace(T(p->text)[i]); ++i )
;
- key.id = T(p->text)+1;
- key.size = i-1;
-
- if ( ret = bsearch(&key, blocktags, SZTAGS, sizeof key, (stfu)casort))
- return ret;
- return 0;
+ return mkd_search_tags(T(p->text)+1, i-1);
}
@@ -204,6 +174,25 @@ splitline(Line *t, int cutpoint)
}
+static Line *
+commentblock(Paragraph *p)
+{
+ Line *t, *ret;
+ char *end;
+
+ for ( t = p->text; t ; t = t->next) {
+ if ( end = strstr(T(t->text), "-->") ) {
+ splitline(t, 3 + (end - T(t->text)) );
+ ret = t->next;
+ t->next = 0;
+ return ret;
+ }
+ }
+ return t;
+
+}
+
+
static Line *
htmlblock(Paragraph *p, struct kw *tag)
{
@@ -212,7 +201,10 @@ htmlblock(Paragraph *p, struct kw *tag)
int c;
int i, closing, depth=0;
- if ( tag->selfclose || (tag->size >= MAXTAG) ) {
+ if ( tag == &comment )
+ return commentblock(p);
+
+ if ( tag->selfclose ) {
ret = f.t->next;
f.t->next = 0;
return ret;
@@ -263,25 +255,6 @@ htmlblock(Paragraph *p, struct kw *tag)
}
-static Line *
-comment(Paragraph *p)
-{
- Line *t, *ret;
- char *end;
-
- for ( t = p->text; t ; t = t->next) {
- if ( end = strstr(T(t->text), "-->") ) {
- splitline(t, 3 + (end - T(t->text)) );
- ret = t->next;
- t->next = 0;
- return ret;
- }
- }
- return t;
-
-}
-
-
/* tables look like
* header|header{|header}
* ------|------{|......}
@@ -384,26 +357,9 @@ ishr(Line *t)
static int
-ishdr(Line *t, int *htyp)
+issetext(Line *t, int *htyp)
{
int i;
-
-
- /* first check for etx-style ###HEADER###
- */
-
- /* leading run of `#`'s ?
- */
- for ( i=0; T(t->text)[i] == '#'; ++i)
- ;
-
- /* ANY leading `#`'s make this into an ETX header
- */
- if ( i && (i < S(t->text) || i > 1) ) {
- *htyp = ETX;
- return 1;
- }
-
/* then check for setext-style HEADER
* ======
*/
@@ -428,6 +384,31 @@ ishdr(Line *t, int *htyp)
}
+static int
+ishdr(Line *t, int *htyp)
+{
+ int i;
+
+
+ /* first check for etx-style ###HEADER###
+ */
+
+ /* leading run of `#`'s ?
+ */
+ for ( i=0; T(t->text)[i] == '#'; ++i)
+ ;
+
+ /* ANY leading `#`'s make this into an ETX header
+ */
+ if ( i && (i < S(t->text) || i > 1) ) {
+ *htyp = ETX;
+ return 1;
+ }
+
+ return issetext(t, htyp);
+}
+
+
static int
isdefinition(Line *t)
{
@@ -762,11 +743,12 @@ listitem(Paragraph *p, int indent)
t->next = 0;
return q;
}
- /* indent as far as the initial line was indented. */
- indent = clip;
+ /* indent at least 2, and at most as
+ * as far as the initial line was indented. */
+ indent = clip ? clip : 2;
}
- if ( (q->dle < indent) && (ishr(q) || islist(q,&z)) && !ishdr(q,&z) ) {
+ if ( (q->dle < indent) && (ishr(q) || islist(q,&z)) && !issetext(q,&z) ) {
q = t->next;
t->next = 0;
return q;
@@ -967,10 +949,7 @@ compile_document(Line *ptr, MMIOT *f)
T(source) = E(source) = 0;
}
p = Pp(&d, ptr, strcmp(tag->id, "STYLE") == 0 ? STYLE : HTML);
- if ( strcmp(tag->id, "!--") == 0 )
- ptr = comment(p);
- else
- ptr = htmlblock(p, tag);
+ ptr = htmlblock(p, tag);
}
else if ( isfootnote(ptr) ) {
/* footnotes, like cats, sleep anywhere; pull them
@@ -1073,15 +1052,15 @@ compile(Line *ptr, int toplevel, MMIOT *f)
}
-static void
-initialize()
+void
+mkd_initialize()
{
static int first = 1;
if ( first-- > 0 ) {
first = 0;
INITRNG(time(0));
- qsort(blocktags, SZTAGS, sizeof blocktags[0], (stfu)casort);
+ mkd_prepare_tags();
}
}
@@ -1111,7 +1090,7 @@ mkd_compile(Document *doc, int flags)
doc->ctx->footnotes = malloc(sizeof doc->ctx->footnotes[0]);
CREATE(*doc->ctx->footnotes);
- initialize();
+ mkd_initialize();
doc->code = compile_document(T(doc->content), doc->ctx);
qsort(T(*doc->ctx->footnotes), S(*doc->ctx->footnotes),
diff --git a/r2/r2/lib/contrib/discount/markdown.h b/r2/r2/lib/contrib/discount/markdown.h
index 26d9269fda..c481e57d22 100644
--- a/r2/r2/lib/contrib/discount/markdown.h
+++ b/r2/r2/lib/contrib/discount/markdown.h
@@ -86,6 +86,7 @@ typedef struct mmiot {
#define NO_PSEUDO_PROTO 0x0040
#define CDATA_OUTPUT 0x0080
#define NOTABLES 0x0400
+#define NOSTRIKETHROUGH 0x0800
#define TOC 0x1000
#define MKD_1_COMPAT 0x2000
#define AUTOLINK 0x4000
@@ -104,6 +105,8 @@ typedef struct mmiot {
* root of the linked list of Lines.
*/
typedef struct document {
+ int magic; /* "I AM VALID" magic number */
+#define VALID_DOCUMENT 0x19600731
Line *headers; /* title -> author(s) -> date */
ANCHOR(Line) content; /* uncompiled text, not valid after compile() */
Paragraph *code; /* intermediate code generated by compile() */
diff --git a/r2/r2/lib/contrib/discount/mkd-extensions.7 b/r2/r2/lib/contrib/discount/mkd-extensions.7
index 7e96d3a40d..1683748bf2 100644
--- a/r2/r2/lib/contrib/discount/mkd-extensions.7
+++ b/r2/r2/lib/contrib/discount/mkd-extensions.7
@@ -20,7 +20,7 @@ The new image syntax is
![alt text](image =/height/x/width/ "title")
.fi
.Ss pseudo-protocols
-Three pseudo-protocols have been added to links
+Five pseudo-protocols have been added to links
.Bl -tag -width XXXXX
.It Ar id:
The
@@ -48,9 +48,16 @@ is discarded.
The
.Ar "alt text"
is marked up and written to the output, wrapped with
-.Em "
+.Em ""
and
.Em "" .
+.It Ar lang:
+The
+.Ar "alt text"
+s marked up and written to the output, wrapped with
+.Em ""
+and
+.Em "" .
.El
.Ss Pandoc headers
If markdown was configured with
@@ -163,6 +170,18 @@ is at the start of a column, it tells
to align the cell contents to the left; if it's at the end, it
aligns right, and if there's one at the start and at the
end, it centers.
+.Ss strikethrough
+A strikethrough syntax is supported in much the same way that
+.Ar `
+is used to define a section of code. If you enclose text with
+two or more tildes, such as
+.Em ~~erased text~~
+it will be written as
+.Em "erased text" .
+Like code sections, you may use as many
+.Ar ~
+as you want, but there must be as many starting tildes as closing
+tildes.
.Sh AUTHOR
David Parsons
.%T http://www.pell.portland.or.us/~orc/
diff --git a/r2/r2/lib/contrib/discount/mkd-functions.3 b/r2/r2/lib/contrib/discount/mkd-functions.3
index 2bbaf2af51..fb49c058e6 100644
--- a/r2/r2/lib/contrib/discount/mkd-functions.3
+++ b/r2/r2/lib/contrib/discount/mkd-functions.3
@@ -127,7 +127,7 @@ accepts the same flags that
and
.Fn mkd_string
do;
-.Bl -tag -width MKD_NOIMAGE -compact
+.Bl -tag -width MKD_NOSTRIKETHROUGH -compact
.It Ar MKD_NOIMAGE
Do not process `![]' and
remove
@@ -159,6 +159,8 @@ function.
.It Ar MKD_1_COMPAT
MarkdownTest_1.0 compatability flag; trim trailing spaces from the
first line of code blocks and disable implicit reference links.
+.It Ar MKD_NOSTRIKETHROUGH
+Disable strikethrough support.
.El
.Sh RETURN VALUES
The functions
diff --git a/r2/r2/lib/contrib/discount/mkdio.c b/r2/r2/lib/contrib/discount/mkdio.c
index 0e930450ed..324329959e 100644
--- a/r2/r2/lib/contrib/discount/mkdio.c
+++ b/r2/r2/lib/contrib/discount/mkdio.c
@@ -24,8 +24,10 @@ new_Document()
Document *ret = calloc(sizeof(Document), 1);
if ( ret ) {
- if (( ret->ctx = calloc(sizeof(MMIOT), 1) ))
+ if (( ret->ctx = calloc(sizeof(MMIOT), 1) )) {
+ ret->magic = VALID_DOCUMENT;
return ret;
+ }
free(ret);
}
return 0;
diff --git a/r2/r2/lib/contrib/discount/mkdio.h b/r2/r2/lib/contrib/discount/mkdio.h
index 3e30c6fb7e..0c26f110ad 100644
--- a/r2/r2/lib/contrib/discount/mkdio.h
+++ b/r2/r2/lib/contrib/discount/mkdio.h
@@ -74,6 +74,7 @@ extern char markdown_version[];
#define MKD_NO_EXT 0x0040 /* don't allow pseudo-protocols */
#define MKD_CDATA 0x0080 /* generate code for xml ![CDATA[...]] */
#define MKD_NOTABLES 0x0400 /* disallow tables */
+#define MKD_NOSTRIKETHROUGH 0x0800/* forbid ~~strikethrough~~ */
#define MKD_TOC 0x1000 /* do table-of-contents processing */
#define MKD_1_COMPAT 0x2000 /* compatability with MarkdownTest_1.0 */
#define MKD_AUTOLINK 0x4000 /* make http://foo.com link even without <>s */
diff --git a/r2/r2/lib/contrib/discount/resource.c b/r2/r2/lib/contrib/discount/resource.c
index 3e5628a963..7f1bc2e1ad 100644
--- a/r2/r2/lib/contrib/discount/resource.c
+++ b/r2/r2/lib/contrib/discount/resource.c
@@ -140,7 +140,7 @@ ___mkd_freeLineRange(Line *anchor, Line *stop)
void
mkd_cleanup(Document *doc)
{
- if ( doc ) {
+ if ( doc && (doc->magic == VALID_DOCUMENT) ) {
if ( doc->ctx ) {
___mkd_freemmiot(doc->ctx, 0);
free(doc->ctx);
diff --git a/r2/r2/lib/contrib/discount/tags.c b/r2/r2/lib/contrib/discount/tags.c
new file mode 100644
index 0000000000..3821699dfb
--- /dev/null
+++ b/r2/r2/lib/contrib/discount/tags.c
@@ -0,0 +1,110 @@
+/* block-level tags for passing html blocks through the blender
+ */
+#define __WITHOUT_AMALLOC 1
+#include "cstring.h"
+#include "tags.h"
+
+STRING(struct kw) blocktags;
+
+
+/* define a html block tag
+ */
+void
+mkd_define_tag(char *id, int selfclose)
+{
+ struct kw *p = &EXPAND(blocktags);
+
+ p->id = id;
+ p->size = strlen(id);
+ p->selfclose = selfclose;
+}
+
+
+/* case insensitive string sort (for qsort() and bsearch() of block tags)
+ */
+static int
+casort(struct kw *a, struct kw *b)
+{
+ if ( a->size != b->size )
+ return a->size - b->size;
+ return strncasecmp(a->id, b->id, b->size);
+}
+
+
+/* stupid cast to make gcc shut up about the function types being
+ * passed into qsort() and bsearch()
+ */
+typedef int (*stfu)(const void*,const void*);
+
+
+/* sort the list of html block tags for later searching
+ */
+void
+mkd_sort_tags()
+{
+ qsort(T(blocktags), S(blocktags), sizeof(struct kw), (stfu)casort);
+}
+
+
+
+/* look for a token in the html block tag list
+ */
+struct kw*
+mkd_search_tags(char *pat, int len)
+{
+ struct kw key;
+
+ key.id = pat;
+ key.size = len;
+
+ return bsearch(&key, T(blocktags), S(blocktags), sizeof key, (stfu)casort);
+}
+
+
+/* load in the standard collection of html tags that markdown supports
+ */
+void
+mkd_prepare_tags()
+{
+
+#define KW(x) mkd_define_tag(x, 0)
+#define SC(x) mkd_define_tag(x, 1)
+
+ static int populated = 0;
+
+ if ( populated ) return;
+ populated = 1;
+
+ KW("STYLE");
+ KW("SCRIPT");
+ KW("ADDRESS");
+ KW("BDO");
+ KW("BLOCKQUOTE");
+ KW("CENTER");
+ KW("DFN");
+ KW("DIV");
+ KW("OBJECT");
+ KW("H1");
+ KW("H2");
+ KW("H3");
+ KW("H4");
+ KW("H5");
+ KW("H6");
+ KW("LISTING");
+ KW("NOBR");
+ KW("UL");
+ KW("P");
+ KW("OL");
+ KW("DL");
+ KW("PLAINTEXT");
+ KW("PRE");
+ KW("TABLE");
+ KW("WBR");
+ KW("XMP");
+ SC("HR");
+ SC("BR");
+ KW("IFRAME");
+ KW("MAP");
+
+ mkd_sort_tags();
+} /* mkd_prepare_tags */
diff --git a/r2/r2/lib/contrib/discount/tags.h b/r2/r2/lib/contrib/discount/tags.h
new file mode 100644
index 0000000000..b5bddb3b2b
--- /dev/null
+++ b/r2/r2/lib/contrib/discount/tags.h
@@ -0,0 +1,18 @@
+/* block-level tags for passing html blocks through the blender
+ */
+#ifndef _TAGS_D
+#define _TAGS_D
+
+struct kw {
+ char *id;
+ int size;
+ int selfclose;
+} ;
+
+
+struct kw* mkd_search_tags(char *, int);
+void mkd_prepare_tags();
+void mkd_sort_tags();
+void mkd_define_tag(char *, int);
+
+#endif
diff --git a/r2/r2/lib/contrib/discount/tests/code.t b/r2/r2/lib/contrib/discount/tests/code.t
index 3227ecf32a..8e8f20c074 100644
--- a/r2/r2/lib/contrib/discount/tests/code.t
+++ b/r2/r2/lib/contrib/discount/tests/code.t
@@ -12,8 +12,12 @@ try 'format for code block html' \
code
'
+try 'mismatched backticks' '```tick``' '
`tick
'
+try 'mismatched backticks(2)' '``tick```' '
``tick```
'
try 'unclosed single backtick' '`hi there' '
`hi there
'
try 'unclosed double backtick' '``hi there' '
``hi there
'
+try 'triple backticks' '```hi there```' '
hi there
'
+try 'quadruple backticks' '````hi there````' '
hi there
'
try 'remove space around code' '`` hi there ``' '
'
+try 'nested lists and a header' \
+ '- A list item
+That goes over multiple lines
+
+ and paragraphs
+
+- Another list item
+
+ + with a
+ + sublist
+
+## AND THEN A HEADER' \
+'
-Feeling hesitant? Try the free version of iReddit. You just get one sound effect, though it is cool, and you can aggregate only 100 of your reddit subscriptions.
-
-
-
-
not convinced? watch the commercial
@@ -228,11 +221,7 @@
the feature list
display thumbnails for most efficient exploration
alien loading animation improves load time by 500% (that is, makes the waiting suck 5x less)
pro-tip: enable sounds in preferences and savor the awe of your peers
diff --git a/r2/r2/templates/frametoolbar.html b/r2/r2/templates/frametoolbar.html
index f1fcc4c460..4e670ab43f 100644
--- a/r2/r2/templates/frametoolbar.html
+++ b/r2/r2/templates/frametoolbar.html
@@ -51,10 +51,6 @@
- %if thing.dorks:
-
- %endif
%if thing._fullname:
diff --git a/r2/r2/templates/giftgold.html b/r2/r2/templates/giftgold.html
new file mode 100644
index 0000000000..c499fefe69
--- /dev/null
+++ b/r2/r2/templates/giftgold.html
@@ -0,0 +1,21 @@
+## The contents of this file are subject to the Common Public Attribution
+## License Version 1.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://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be consistent
+## with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is Reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of
+## the Original Code is CondeNet, Inc.
+##
+## All portions of the code written by CondeNet are Copyright (c) 2006-2010
+## CondeNet, Inc. All Rights Reserved.
+################################################################################
diff --git a/r2/r2/templates/gold.html b/r2/r2/templates/gold.html
new file mode 100644
index 0000000000..9ca668a442
--- /dev/null
+++ b/r2/r2/templates/gold.html
@@ -0,0 +1,180 @@
+## The contents of this file are subject to the Common Public Attribution
+## License Version 1.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://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be consistent
+## with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is Reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of
+## the Original Code is CondeNet, Inc.
+##
+## All portions of the code written by CondeNet are Copyright (c) 2006-2010
+## CondeNet, Inc. All Rights Reserved.
+################################################################################
+
+<%namespace file="utils.html" import="error_field, success_field"/>
+<%namespace name="utils" file="utils.html"/>
+<%namespace file="createsubreddit.html" import="radio_type"/>
+
+<%! from r2.lib.strings import strings, Score %>
+
+
+
+<%def name="goldsec(goldtype)">
+
+%def>
+
+<%def name="gold_dropdown(what)">
+
+
+%def>
+
+
+
+
+
+ <%self:goldsec goldtype="autorenew">
+ <%utils:round_field title="${'Monthly or yearly?'}">
+ ${radio_type("period", "monthly", _("$3.99 / month"), "", False)}
+ ${radio_type("period", "yearly", _("$29.99 / year (which works out to just $2.50 / month!)"),
+ "", True)}
+
+
+ %utils:round_field>
+ %self:goldsec>
+
+ <%self:goldsec goldtype="onetime">
+ <%utils:round_field title="${_('How many months?')}">
+ ${gold_dropdown("months")}
+
+
+ %utils:round_field>
+ %self:goldsec>
+
+ <%self:goldsec goldtype="gift">
+ <%utils:round_field title="${_('How much gold do you wish to bestow?')}">
+ %if thing.user_creddits:
+
+
+
+ ${_("Note: If you want to give more than %(number)d, you'll need to buy more creddits.") % dict(number=thing.user_creddits)}
+
+ %else:
+ ${gold_dropdown("months")}
+ %endif
+
+
+ ${_("To whom are you giving said gold?")}
+
+ ${error_field("NO_USER", "recipient", "span")}
+ ${error_field("USER_DOESNT_EXIST", "recipient", "span")}
+ %if thing.bad_recipient:
+
+ ${_("that user doesn't exist")}
+ %endif
+
+
+
+ ${_("Do you want them to know it came from you?")}
+
+ ${radio_type("signed", "yes", _("yes, include my username"),
+ "",
+ True)}
+
+ ${radio_type("signed", "no", _("no, make it anonymous"),
+ "",
+ False)}
+
+
+
+ ${_("Gift message (optional):")}
+
+
+
+
+
+
+ %utils:round_field>
+ %self:goldsec>
+
+ <%self:goldsec goldtype="creddits">
+ <%utils:round_field title="${_('How many creddits would you like to buy? (each will allow you to give one month of reddit gold)')}">
+ ${gold_dropdown("creddits")}
+
+
+ %utils:round_field>
+ %self:goldsec>
+
+
diff --git a/r2/r2/templates/goldpayment.html b/r2/r2/templates/goldpayment.html
new file mode 100644
index 0000000000..04dbd3e0b4
--- /dev/null
+++ b/r2/r2/templates/goldpayment.html
@@ -0,0 +1,99 @@
+## The contents of this file are subject to the Common Public Attribution
+## License Version 1.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://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be consistent
+## with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is Reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of
+## the Original Code is CondeNet, Inc.
+##
+## All portions of the code written by CondeNet are Copyright (c) 2006-2010
+## CondeNet, Inc. All Rights Reserved.
+################################################################################
+
+<%namespace file="utils.html" import="error_field, success_field"/>
+<%namespace name="utils" file="utils.html"/>
+<%namespace file="createsubreddit.html" import="radio_type"/>
+
+<%
+ from r2.lib.filters import unsafe, safemarkdown
+%>
+
+
+ ${_("[deleted]")} ${thing.timesince} ${_("ago")} +
%else:- - ${thing.author.name} - |${thing.score} ${ungettext("point", "points", thing.score)} -${_("written")} ${thing.timesince} ${_("ago")} + + ${thing.author.name} + |${thing.score} ${ungettext("point", "points", thing.score)} + ${_("written")} ${thing.timesince} ${_("ago")}
${unsafe(safemarkdown(thing.body, nofollow=thing.nofollow))} %endif +