r111084 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r111083‎ | r111084 | r111085 >
Date:20:25, 9 February 2012
Author:aaron
Status:ok
Tags:
Comment:
* Removed obsolete files
* Moved test files to /tests
Modified paths:
  • /trunk/extensions/SwiftMedia/SwiftMedia.body.php (deleted) (history)
  • /trunk/extensions/SwiftMedia/SwiftMedia.i18n.php (deleted) (history)
  • /trunk/extensions/SwiftMedia/SwiftMedia.php (deleted) (history)
  • /trunk/extensions/SwiftMedia/TODO (deleted) (history)
  • /trunk/extensions/SwiftMedia/smtest.py (deleted) (history)
  • /trunk/extensions/SwiftMedia/test_rewrite.py (deleted) (history)
  • /trunk/extensions/SwiftMedia/wmf/tests/smtest.py (added) (history)
  • /trunk/extensions/SwiftMedia/wmf/tests/test_rewrite.py (added) (history)

Diff [purge]

Index: trunk/extensions/SwiftMedia/SwiftMedia.i18n.php
@@ -1,8 +0,0 @@
2 -<?php
3 -$messages = array();
4 -
5 -$messages['en'] = array(
6 - 'swiftmedia' => 'Openstack\'s Swift is a very large scale reliable object store. It will serve multiple petabytes of media files.
7 -[[Extension:SwiftMedia]] allows MediaWiki uploads to be stored in Swift.',
8 -);
9 -
Index: trunk/extensions/SwiftMedia/SwiftMedia.php
@@ -1,21 +0,0 @@
2 -<?php
3 -$wgExtensionCredits['other'][] = array(
4 - 'path' => __FILE__, // File name for the extension itself, required for getting the revision number from SVN - string, adding in 1.15
5 - 'name' => "SwiftMedia", // Name of extension - string
6 - 'descriptionmsg' => "swiftmedia", // Same as above but name of a message, for i18n - string, added in 1.12.0
7 - 'version' => 0, // Version number of extension - number or string
8 - 'author' => "Russ Nelson", // The extension author's name - string or array for multiple
9 - 'url' => "http://www.mediawiki.org/wiki/Extension:SwiftMedia", // URL of extension (usually instructions) - string
10 -);
11 -
12 -$wgAutoloadClasses['SwiftFile'] =
13 - $wgAutoloadClasses['SwiftForeignDBFile'] =
14 - $wgAutoloadClasses['SwiftForeignDBRepo'] =
15 - $wgAutoloadClasses['SwiftForeignDBviaLBRepo'] =
16 - $wgAutoloadClasses['SwiftRepo'] = dirname( __FILE__ ) . '/SwiftMedia.body.php';
17 -$wgAutoloadClasses['CF_Authentication'] =
18 - $wgAutoloadClasses['CF_Connection'] =
19 - $wgAutoloadClasses['CF_Container'] =
20 - $wgAutoloadClasses['CF_Object'] = '/usr/share/php-cloudfiles/cloudfiles.php';
21 -
22 -$wgExtensionMessagesFiles['swiftmedia'] = dirname( __FILE__ ) . '/SwiftMedia.i18n.php';
Index: trunk/extensions/SwiftMedia/TODO
@@ -1,108 +0,0 @@
2 -Pending:
3 -
4 -This group all relate to the 404 handler:
5 -6) There's no 404 handler to generate missing thumbnails.
6 -7) There's no support for remote thumbnailing.
7 -7.1) The SpecialUploadStash, when it calls the remote scaler, is actually just relying on the 404 handler on upload.wm.org.
8 - It's just executing thumb.php (is it thumb??) to make a thumbnail on a temporary name. Note that it's using a temp/
9 - directory in the thumbs container, not a directory in the temp container.
10 -7.2) The scaler cluster is simply some machines with access to the originals and thumb storage. The 404 handler running on the Apache
11 - front-ends forwards the request to a thumb.php running on the scaler cluster. Thumb.php takes care of creating the thumbnail.
12 -6&7) Basically, the code which currently fetches 404 thumbs from upload.wikimedia.org needs to be changed slightly so that it inserts
13 - thumb.php with the appropriate parameters and fetches from the scaler cluster.
14 -19) TS: "I suggest you test transform errors throughout your whole system. With our current NFS system, transform errors work by having thumb.php return an HTTP 500 response with an informative HTML error message. The error message is passed through to Squid by the 404 handler, and Squid won't cache it. The user will see a broken image icon in their browser, and choosing "view image" from the context menu of the image will show them the error message from thumb.php."
15 -
16 -12) Why is anybody calling resolveVirtualUrl()? It's defined in the Repo, but getPath() is defined against a file.
17 -Why is UploadStashFile() being called with a virtual URL? Once the file has been stashed() it has an object name. The container name is implicit.
18 -Should UploadStashFile *always* (in our case) be called with a virtual URL?
19 -23) TS: "When you get around to implementing SwiftRepo::append(), it will need some sort of concurrency control to avoid having chunks overwrite each other. "
20 -24) TS: "SwiftRepo::swiftcopy() should return a Status object instead of throwing exceptions."
21 -26) Hazmat wonders what happens if two clients fetch missing thumbnails at the same time. What happens when you have overlapping writes to the same object.
22 -
23 -Partially resolved:
24 -
25 -20) TS: "It's not appropriate to allow CloudFiles exceptions to be propagated back to the callers of File/FileRepo methods such as transform(). Doing this will cause the page to not be displayed at all, with an exceptionally ugly error message in its place. FileRepo has a system for returning user-friendly error messages from pretty much anywhere. For example, SwiftRepo::storeBatch() has a Status object to hold error messages, but your code just sets it to a "good" result, it never sets any errors in it."
26 -"You can throw an MWException in response to configuration errors, or assertion-like unexpected errors, but it's not appropriate to be throwing exceptions in response to network errors or errors generated by the Swift server.
27 -
28 -Resolved:
29 -
30 -21a) TS: "// Check overwriting
31 - if (0) { #FIXME
32 -
33 -Fix that. It's important to avoid data loss in file rename operations."
34 -22) TS: "I think this [MD65] validation feature in FSRepo is just a hack for NFS. Okay to remove it."
35 -5) The Upload seems to take more time than I expect, but that could be a function of generating the six thumbnails.
36 - It *is* a function of generating the seven (we generate 800x600 twice) thumbnails. Each one takes 1/2 second.
37 -8) Test cases (but of course that could be done until the cows come home).
38 -9) Read through the code and look for anything which is insane.
39 -10) Remove directory from $wgLocalFileRepo, to make sure that there's no references to it. Ditto for wgDeletedDirectory and deletedDir.
40 - wgDeletedDirectory and deletedDir can be removed.
41 -11) Determine what to do about the one remaining core change needed for Swift.
42 -12) Implement repo->freeTemp() - needed by several extensions and UploadFromStash.
43 -13) Do we need $wgLocalRepo->ThumbUrl to be configurable given that the Python middleware presumes it?
44 - We currently have no need for it to be configurable in Swift. I'll just hard-code it to .../thumb with a note saying
45 - that if it gets changed here, it needs to be changed in the Swift middleware as well.
46 -14) TS: "File::transform() needs to be split into two functions, one with the code that you're duplicating, and the other with the code that you're overriding."
47 -15) TS: "Scripted transform needs to keep working, ... We use it for private wikis. The "transform via 404" feature needs to be left in as well."
48 -16) TS: "You need to re-add an equivalent for the file_exists($thumbPath) check that's in File::transform()."
49 -17) TS: "LocalFile uses the 404 handler. Swift will probably use the 404 handler. That's why you need to support canTransformVia404()."
50 -18) TS: "You need to handle errors from MediaHandler::doTransform() correctly. "
51 -20) TS: "SwiftMedia::migrateThumbFile() should just do nothing,"
52 -21) TS: "[$wgExcludeFromThumbnailPurge] looks pretty trivial to implement to me." (RN: It was!)
53 -25) TS: In the short term, just copy ForeignDBViaLBRepo to ForeignDBViaSMRepo and change the class parent.
54 -
55 -
56 -
57 -neilk_: okay, the moment when an upload passes from being a temp file into something else is at $upload->processUpload()
58 -neilk_: in the old design, in essence, all this does is move a file into an NFS directory, and creates the matching database entry which creates a wiki page.
59 -neilk_: so far I don't think this should be news?
60 -nelson____: right
61 -neilk_: So that's includes/specials/SpecialUpload.php
62 -neilk_: then there's includes/api/ApiUpload.php
63 -neilk_: which is similar but not quite the same
64 -
65 -Watch for that!
66 -
67 -neilk_: in ApiUpload.php there is the option to stash explicitly
68 -neilk_: so the path is a bit convoluted in ApiUpload.php. Also if I remember right the file is accessed a bit differently
69 -neilk_: it is possible to have stashing in ALL of these cases
70 -neilk_: but in SpecialUpload, stashing occurs if there's a recoverable error with the file, like a bad file name
71 -neilk_: in ApiUpload, stashing can happen for that reason, or it can happens if you ask for it explicitly (which is how UploadWizard works).
72 -neilk_: nelson__: anyway is this answering your question yet?
73 -nelson____: yes.
74 -neilk_: ok so that's the overview of the upload methods & stashing, what else
75 -nelson____: I think that part of the problem is that various parts of the system feel free to make the jump from "stored" to "accessible as full path".
76 -neilk_: yes
77 -neilk_: it drove me nuts too
78 -neilk_: and the code intentionally conflates a number of cases, because MediaWiki at heart just wants to throw a number of files into a directory, not manage millions of them
79 -nelson____: I gotta figure out how to mark the difference, so that something is either 1) a locally stored file, or 2) a blind token from the repo.
80 -neilk_: can't you subclass FileRepo then?
81 -nelson____: cuz if you have the blind token, then you need to turn it into a File and then call getPath() on it.
82 -nelson____: but there's times when the upload code expects to be able to access a file without creating a File first.
83 -neilk_: when does upload code access a file that isn't a File?
84 -nelson____: sec
85 -nelson____: neilk_: UploadStashFile does this in its __construct: $path = $repo->resolveVirtualUrl( $path );
86 -neilk_: yes
87 -nelson____: But maybe the key thing for me to know is that when it's SwiftMedia, $path on the right is *always* a mwrepo/
88 -nelson____: If that's the case, then I think I'm okay. I'm just having trouble following the code up and out and then back down.
89 -neilk_: there isn't any code, to my knowledge, which assumes that $path is a "physical" path. It uses the repo methods.
90 -neilk_: I don't blame you if you're having trouble.
91 -neilk_: isn't any code in UploadStashFile, I mean.
92 -nelson____: maybe ... what I should do is throw an exception if SwiftRepo::resolveVirtualUrl ever gets called without a mwrepo url, and then just go test everything.
93 -nelson____: I think maybe I'm trying to overanalyze the code.
94 -neilk_: I sympathize
95 -nelson____: I should trust the code more.
96 -neilk_: hm, I think not
97 -nelson____: Trust but verify.
98 -neilk_: also, this is sad but stashing is done in two slightly different ways, too.
99 -nelson____: I saw.
100 -neilk_: but compatible
101 -neilk_: I wanted UploadStash to absorb the other one.
102 -neilk_: We can still do that.
103 -nelson____: agreed.
104 -neilk_: When I was in the middle of Upload code, I always felt like I was cramped in some access area between two walls, with all the pipes and electrical work.
105 -nelson____: interesting metaphor.
106 -nelson____: yeah, I think part of that problem is you're always stuck between the database and the filestore.
107 -nelson____: they both have opinions about how things work, and you have to keep them consistent.
108 -nelson____: Okay, I'm gonna take some notes then go home. taking the weekend off for a bicycle road trip.
109 -
Index: trunk/extensions/SwiftMedia/smtest.py
@@ -1,169 +0,0 @@
2 -#!/usr/bin/python
3 -# http://www.deheus.net/petrik/blog/2005/11/20/creating-a-wikipedia-watchlist-rss-feed-with-python-and-twill/
4 -
5 -import sys, string, datetime, time, os, re, stat
6 -import twill
7 -import twill.commands as t
8 -import gd
9 -
10 -temp_html = "/tmp/wikipedia.html"
11 -rss_title = "Wikipedia watchlist"
12 -rss_link = "http://en.wikipedia.org"
13 -host = "http://ersch.wikimedia.org/"
14 -#host = "http://127.0.0.1/wiki/"
15 -
16 -def login(username, password):
17 - t.add_extra_header("User-Agent", "python-twill-russnelson@gmail.com")
18 -
19 - t.go(host+"index.php/Special:UserLogin")
20 - t.fv("1", "wpName", username)
21 - t.fv("1", "wpPassword", password)
22 - t.submit("wpLoginAttempt")
23 -
24 -
25 -def upload_list(browser, pagename, uploads):
26 -
27 - # get the file sizes for later comparison.
28 - filesizes = []
29 - for fn in uploads:
30 - filesizes.append(os.stat(fn)[stat.ST_SIZE])
31 - filesizes.reverse() # because they get listed newest first.
32 -
33 - # Upload copy #1.
34 - t.go(host+"index.php/Special:Upload")
35 - t.formfile("1", "wpUploadFile", uploads[0])
36 - t.fv("1", "wpDestFile", pagename)
37 - t.fv("1", "wpUploadDescription", "Uploading %s" % pagename)
38 - t.submit("wpUpload")
39 -
40 - # Verify that we succeeded.
41 - t.find("File:%s" % pagename)
42 -
43 - for fn in uploads[1:]:
44 - # propose that we upload a replacement
45 - t.go(host+"index.php?title=Special:Upload&wpDestFile=%s&wpForReUpload=1" % pagename)
46 - t.formfile("1", "wpUploadFile", fn)
47 - t.fv("1", "wpUploadDescription", "Uploading %s as %s" % (fn, pagename))
48 - t.submit("wpUpload")
49 -
50 - # get the URLs for the thumbnails
51 - urls = []
52 - for url in re.finditer(r'<td><a href="([^"]*?)"><img alt="Thumbnail for version .*?" src="(.*?)"', browser.get_html()):
53 - urls.append(url.group(1))
54 - urls.append(url.group(2))
55 -
56 - print filesizes
57 - for i, url in enumerate(urls):
58 - t.go(url)
59 - if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]:
60 - print i,len(browser.get_html()), filesizes[i / 2]
61 - t.find("Files differ in size")
62 - t.code("200")
63 - t.back()
64 -
65 - # delete all versions
66 - t.go(host+"index.php?title=File:%s&action=delete" % pagename)
67 - # after we get the confirmation page, commit to the action.
68 - t.fv("1", "wpReason", "Test Deleting...")
69 - t.submit("mw-filedelete-submit")
70 -
71 - # make sure that we can't visit their URLs.
72 - for i, url in enumerate(urls):
73 - t.go(url)
74 - if 0 and i % 2 == 1 and i > 0 and browser.get_code() == 200:
75 - # bug 30192: the archived file's thumbnail doesn't get deleted.
76 - print "special-casing the last URL"
77 - continue
78 - t.code("404")
79 -
80 - # restore the current and archived version.
81 - t.go(host+"index.php/Special:Undelete/File:%s" % pagename)
82 - t.fv("1", "wpComment", "Test Restore")
83 - t.submit("restore")
84 -
85 - # visit the page to make sure that the thumbs get re-rendered properly.
86 - # when we get the 404 handler working correctly, this won't be needed.
87 - t.go(host+"index.php?title=File:%s" % pagename)
88 -
89 - # make sure that they got restored correctly.
90 - for i, url in enumerate(urls):
91 - t.go(url)
92 - if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]:
93 - t.find("Files differ in size")
94 - t.code("200")
95 - t.back()
96 -
97 - if len(uploads) != 2:
98 - return
99 -
100 - match = re.search(r'"([^"]+?)" title="[^"]+?">revert', browser.get_html())
101 - if not match:
102 - t.find('revert')
103 - t.go(match.group(1).replace('&amp;', '&'))
104 -
105 -def make_files(pagename):
106 - redfilename = "/tmp/Red-%s" % pagename
107 - greenfilename = "/tmp/Green-%s" % pagename
108 - bluefilename = "/tmp/Blue-%s" % pagename
109 -
110 - # create a small test image.
111 - gd.gdMaxColors = 256
112 - i = gd.image((200,100))
113 - black = i.colorAllocate((0,0,0))
114 - white = i.colorAllocate((255,255,255))
115 - red = i.colorAllocate((255,55,55))
116 - green = i.colorAllocate((55,255,55))
117 - blue = i.colorAllocate((55,55,255))
118 -
119 - # now write a red version
120 - i.rectangle((0,0),(199,99),red, red)
121 - i.line((0,0),(199,99),black)
122 - i.string(gd.gdFontLarge, (5,50), pagename, white)
123 - i.writePng(redfilename)
124 -
125 - # now write a green version
126 - i.rectangle((0,0),(199,99),green, green)
127 - i.line((0,0),(99,99),black)
128 - i.string(gd.gdFontLarge, (5,50), pagename, white)
129 - i.writePng(greenfilename)
130 -
131 - # write a blue version
132 - i.rectangle((0,0),(199,99),blue,blue)
133 - i.line((0,0),(99,199),black)
134 - i.string(gd.gdFontLarge, (5,50), pagename, white)
135 - i.writePng(bluefilename)
136 -
137 - # propose that we delete it (in case it exists)
138 - t.go(host+"index.php?title=File:%s&action=delete" % pagename)
139 - # make sure that we've NOT gotten the wrong page and HAVE gotten the right one.
140 - t.notfind('You are about to delete the file')
141 - t.find("could not be deleted")
142 -
143 - return (redfilename, greenfilename, bluefilename )
144 -
145 -def main():
146 - try:
147 - username = sys.argv[1]
148 - password = sys.argv[2]
149 - except IndexError:
150 - print "Please supply username password"
151 - sys.exit(1)
152 - browser = twill.get_browser()
153 - login(username, password)
154 -
155 - serial = time.time()
156 - pagename = "Test-%s.png" % serial
157 - filenames = make_files(pagename)
158 - upload_list(browser, pagename, filenames[0:2])
159 -
160 - # try it again with two replacement files.
161 -# pagename = "Test-%sA.png" % serial
162 -# filenames = make_files(pagename)
163 -# upload_list(browser, pagename, filenames)
164 -
165 - t.showforms()
166 - t.save_html("/tmp/testabcd")
167 -
168 -if __name__ == "__main__":
169 - main()
170 -
Index: trunk/extensions/SwiftMedia/test_rewrite.py
@@ -1,171 +0,0 @@
2 -#!/usr/bin/python
3 -
4 -import unittest
5 -
6 -import webob
7 -
8 -from wmf import rewrite
9 -from wmf.client import ClientException
10 -
11 -class FakeApp(object):
12 - def __init__(self, status, headers):
13 - self.status = status
14 - self.headers = headers
15 -
16 - def __call__(self, env, start_response):
17 - start_response(self.status, self.headers)
18 - return "FAKE APP"
19 -
20 -def start_response(*args):
21 - pass
22 -
23 -class TestRewrite(unittest.TestCase):
24 -
25 - def setUp(self):
26 - pass
27 -
28 - account="AUTH_..."
29 - urlbig = 'http://alsted.wikimedia.org/wikipedia/commons/a/aa/'\
30 - 'Dzimbo_u_Beogradu_19.jpeg'
31 - urlorig = '/v1/' + account + \
32 - '/wikipedia%2Fcommons/a/aa/Dzimbo_u_Beogradu_19.jpeg'
33 - thumbbig = 'http://alsted.wikimedia.org/wikipedia/commons/thumb/a/aa/'\
34 - 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg'
35 - url448 = '/v1/' + account + \
36 - '/wikipedia%2Fcommons%2Fthumb/a/aa/'\
37 - 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg'
38 - urlaccount = 'http://alsted.wikimedia.org/' + account
39 - contname = '/wikipedia/commons/thumb'
40 - objname = '/a/aa/Dzimbo_u_Beogradu_19.jpeg/91px-Dzimbo_u_Beogradu_19.jpeg'
41 - a = dict(account=account,
42 - url="https://127.0.0.1:11000/v1.0",
43 - login="yourlogin",
44 - thumbhost='localhost',
45 - user_agent='Mozilla/5.0',
46 - key="yourkey")
47 -
48 - def test_01(self):
49 - """#01 Cur controller can snarf its args."""
50 - controller = rewrite.ObjectController()
51 - controller.do_start_response("200 Good", {"test": "testy"})
52 - self.assertEquals(controller.response_args[0], "200 Good")
53 - self.assertEquals(controller.response_args[1], {"test": "testy"})
54 -
55 - def test_01a(self):
56 - """#01a Our app calls into the FakeApp; returns its results if 200 """
57 - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
58 - req = webob.Request.blank(self.urlbig,
59 - environ={'REQUEST_METHOD': 'GET'})
60 - controller = rewrite.ObjectController()
61 - resp = app(req.environ, controller.do_start_response)
62 - self.assertEquals(resp, 'FAKE APP')
63 -
64 - def test_02(self):
65 - """#02 Test URL rewriting for originals. """
66 - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
67 - req = webob.Request.blank(self.urlbig,
68 - environ={'REQUEST_METHOD': 'GET'})
69 - resp = app(req.environ, start_response)
70 - self.assertEquals(req.environ['PATH_INFO'], self.urlorig)
71 -
72 - def test_03(self):
73 - """#03 Test URL rewriting for thumbs. """
74 - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
75 - req = webob.Request.blank(self.thumbbig,
76 - environ={'REQUEST_METHOD': 'GET'})
77 - resp = app(req.environ, start_response)
78 - self.assertEquals(req.environ['PATH_INFO'], self.url448)
79 -
80 - def test_04(self):
81 - """#04 Test a write. Could fail if our token has gone stale"""
82 - app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a)
83 - req = webob.Request.blank(self.thumbbig,
84 - environ={'REQUEST_METHOD': 'GET'})
85 - resp = app(req.environ, start_response)
86 - self.assertEquals(req.environ['PATH_INFO'], self.url448)
87 - # note that we PUT this file onto the server here, even if it's already there.
88 - datalen = 0
89 - for data in resp:
90 - datalen += len(data)
91 - self.assertEquals(datalen, 51543)
92 -
93 - def test_05(self):
94 - """#05 Report 401 (authorization) errors"""
95 - app = rewrite.WMFRewrite(FakeApp("401 Bad", {}),self.a)
96 - req = webob.Request.blank(self.thumbbig,
97 - environ={'REQUEST_METHOD': 'GET'})
98 - resp = app(req.environ, start_response)
99 - self.assertEquals(req.environ['PATH_INFO'], self.url448)
100 - self.assertEquals(resp,
101 - ['401 Unauthorized\n\nThis server could not verify that you are '\
102 - 'authorized to access the document you requested. Either you '\
103 - 'supplied the wrong credentials (e.g., bad password), or your '\
104 - 'browser does not understand how to supply the credentials '\
105 - 'required.\n\n Token may have timed out '])
106 -
107 - def test_06(self):
108 - """#06 Give them a bad token so that the PUT fails."""
109 - app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a)
110 - app.token = "HaHaYeahRight"
111 - req = webob.Request.blank(self.thumbbig,
112 - environ={'REQUEST_METHOD': 'GET'})
113 - resp = app(req.environ, start_response)
114 - self.assertEquals(req.environ['PATH_INFO'], self.url448)
115 - # the PUT fails because we give them a bad token, but ... we should
116 - # really silently just hand back the file.
117 - try:
118 - datalen = 0
119 - for data in resp:
120 - datalen += len(data)
121 - except ClientException, x:
122 - self.assertEquals(datalen, 51543)
123 - y = "ClientException('Object PUT failed',)"
124 - self.assertEquals(`x`, y)
125 -
126 - def test_07(self):
127 - """#07 Make sure that an already-authorized path goes unchanged."""
128 - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
129 - url = self.urlaccount + self.contname + self.objname
130 - req = webob.Request.blank(url, environ={'REQUEST_METHOD': 'GET'})
131 - resp = app(req.environ, start_response)
132 - self.assertEquals(req.url, url) # should remain unchanged
133 - #self.assertTrue(len(app.response_args) == 0) # no args either.
134 -
135 - def test_08(self):
136 - """#08 Don't let them read the container"""
137 - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
138 - req = webob.Request.blank(
139 - 'http://alsted.wikimedia.org/wikipedia/commons/',
140 - environ={'REQUEST_METHOD': 'GET'})
141 - resp = app(req.environ, start_response)
142 - self.assertEquals(resp,
143 - ['403 Forbidden\n\nAccess was denied to this resource.\n\n '\
144 - 'No container listing '])
145 -
146 - # test_09 became obsolete
147 -
148 - def test_10(self):
149 - """#10 Trap weird-ass errors"""
150 - app = rewrite.WMFRewrite(FakeApp("999 Unrecognized", {}),self.a)
151 - req = webob.Request.blank("http://localhost/a/b/c",
152 - environ={'REQUEST_METHOD': 'GET'})
153 - resp = app(req.environ, start_response)
154 - self.assertEquals(resp,
155 - ['501 Not Implemented\n\nThe server has either erred or is '\
156 - 'incapable of performing the requested operation.\n\n Unknown '\
157 - 'Status: 999 '])
158 -
159 - def test_11(self):
160 - """#11 Trap URLs that don't match the regexp"""
161 - app = rewrite.WMFRewrite(FakeApp("404 TryRegexp", {}),self.a)
162 - req = webob.Request.blank("http://localhost/a",
163 - environ={'REQUEST_METHOD': 'GET'})
164 - resp = app(req.environ, start_response)
165 - self.assertEquals(resp,
166 - ['400 Bad Request\n\nThe server could not comply with the '\
167 - 'request since it is either malformed or otherwise '\
168 - 'incorrect.\n\n Regexp failed: "/a" '])
169 -
170 -if __name__ == '__main__':
171 - unittest.main()
172 -
Index: trunk/extensions/SwiftMedia/SwiftMedia.body.php
@@ -1,1444 +0,0 @@
2 -<?php
3 -/**
4 - * Local file in the wiki's own database, only stored in Swift
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * Class to represent a local file in the wiki's own database, only stored in Swift
12 - *
13 - * Provides methods to retrieve paths (physical, logical, URL),
14 - * to generate image thumbnails or for uploading.
15 - *
16 - * Note that only the repo object knows what its file class is called. You should
17 - * never name a file class explictly outside of the repo class. Instead use the
18 - * repo's factory functions to generate file objects, for example:
19 - *
20 - * RepoGroup::singleton()->getLocalRepo()->newFile($title);
21 - *
22 - * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
23 - * in most cases.
24 - *
25 - * @ingroup FileRepo
26 - */
27 -class SwiftFile extends LocalFile {
28 - /**#@+
29 - * @private
30 - */
31 - var
32 - $conn, # our connection to the Swift proxy.
33 - $fileExists, # does the file file exist on disk? (loadFromXxx)
34 - $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
35 - $swiftuser,
36 - $swiftkey,
37 - $authurl,
38 - $container;
39 - /**#@-*/
40 -
41 - /**
42 - * Create a LocalFile from a title
43 - * Do not call this except from inside a repo class.
44 - *
45 - * Note: $unused param is only here to avoid an E_STRICT
46 - *
47 - * @return SwiftFile
48 - */
49 - static function newFromTitle( $title, $repo, $unused = null ) {
50 - if ( empty( $title ) ) {
51 - return null;
52 - }
53 - return new self( $title, $repo );
54 - }
55 -
56 - /**
57 - * Create a LocalFile from a title
58 - * Do not call this except from inside a repo class.
59 - */
60 - static function newFromRow( $row, $repo ) {
61 - $title = Title::makeTitle( NS_FILE, $row->img_name );
62 - $file = new self( $title, $repo );
63 - $file->loadFromRow( $row );
64 -
65 - return $file;
66 - }
67 -
68 - /**
69 - * Constructor.
70 - * Do not call this except from inside a repo class.
71 - */
72 - function __construct( $title, $repo ) {
73 - if ( !is_object( $title ) ) {
74 - throw new MWException( __CLASS__ . ' constructor given bogus title.' );
75 - }
76 -
77 - parent::__construct( $title, $repo );
78 -
79 - $this->tempPaths = array(); // Hash from rel to local copy.
80 - }
81 -
82 - /** splitMime inherited */
83 - /** getName inherited */
84 - /** getTitle inherited */
85 - /** getURL inherited */
86 - /** getViewURL inherited */
87 - /** isVisible inherited */
88 -
89 - /**
90 - * We're re-purposing getPath() to checkout a copy of the file, if we don't already have a copy.
91 - *
92 - * @return string Path to a local copy of the file.
93 - */
94 - public function getPath() {
95 - if ( !array_key_exists( '', $this->tempPaths ) ) {
96 - $this->tempPaths[''] = $this->repo->getLocalCopy( $this->repo->container, $this->getRel(), "getPath_" );
97 - }
98 - return $this->tempPaths[''];
99 - }
100 -
101 - /**
102 - * We're re-purposing getPath() to checkout a copy of the file, if we don't already have a copy.
103 - * Get a local copy of a particular archived file specified by $suffix
104 - *
105 - * @param string suffix Specific archived copy.
106 - * @return string Path to a local copy of the file.
107 - */
108 - public function getArchivePath( $suffix = false ) {
109 - if ( !$suffix ) {
110 - throw new MWException( "Can't call getArchivePath without a suffix" );
111 - }
112 - $rel = $this->getArchiveRel( $suffix );
113 - if ( !array_key_exists( $rel, $this->tempPaths ) ) {
114 - $this->tempPaths[$rel] = $this->repo->getLocalCopy( $this->repo->container, $rel );
115 - }
116 - return $this->tempPaths[$rel];
117 - }
118 -
119 - /**
120 - * We're re-purposing getPath() to checkout a copy of the file, if we don't already have a copy.
121 - * Get a local copy of a particular thumb specified by $suffix
122 - *
123 - * @param string suffix Specific thumbnail.
124 - * @return string Path to a local copy of the file.
125 - */
126 - public function getThumbPath( $suffix = false ) {
127 - if ( !$suffix ) {
128 - throw new MWException( "Can't call getThumbPath without a suffix" );
129 - }
130 - $rel = $this->getRel() . '/' . $suffix;
131 - if ( !array_key_exists( $rel, $this->tempPaths ) ) {
132 - $this->tempPaths[$rel] = $this->repo->getLocalCopy( $this->repo->getZoneContainer( 'thumb' ), $rel );
133 - }
134 - return $this->tempPaths[$rel];
135 - }
136 -
137 - function __destruct() {
138 - foreach ( $this->tempPaths as $path ) {
139 - // Clean up temporary data.
140 - wfDebug( __METHOD__ . ": deleting $path\n" );
141 - unlink( $path );
142 - }
143 - $this->tempPaths = array();
144 - }
145 -
146 - /**
147 - * Do the work of a transform (from an original into a thumb).
148 - * Contains filesystem-specific functions.
149 - *
150 - * @param $thumbName string: the name of the thumbnail file.
151 - * @param $thumbUrl string: the URL of the thumbnail file.
152 - * @param $params Array: an associative array of handler-specific parameters.
153 - * Typical keys are width, height and page.
154 - * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
155 - *
156 - * @return MediaTransformOutput | false
157 - */
158 - function maybeDoTransform( $thumbName, $thumbUrl, $params, $flags ) {
159 - global $wgIgnoreImageErrors, $wgThumbnailEpoch, $wgTmpDirectory;
160 -
161 - // get a temporary place to put the original.
162 - $thumbPath = tempnam( $wgTmpDirectory, 'transform_out_' );
163 - unlink( $thumbPath );
164 - $thumbPath .= '.' . pathinfo( $thumbName, PATHINFO_EXTENSION );
165 -
166 -
167 - if ( $this->repo && $this->repo->canTransformVia404() && !( $flags & self::RENDER_NOW ) ) {
168 - return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
169 - }
170 -
171 - // see if the file exists, and if it exists, is not too old.
172 - $conn = $this->repo->connect();
173 - $container = $this->repo->get_container( $conn, $this->repo->container . '-thumb' );
174 - try {
175 - $pic = $container->get_object( $this->getRel() . "/$thumbName" );
176 - } catch ( NoSuchObjectException $e ) {
177 - $pic = NULL;
178 - }
179 - if ( $pic ) {
180 - $thumbTime = $pic->last_modified;
181 - $tm = strptime( $thumbTime, '%a, %d %b %Y %H:%M:%S GMT' );
182 - $thumbGMT = gmmktime( $tm['tm_hour'], $tm['tm_min'], $tm['tm_sec'], $tm['tm_mon'] + 1, $tm['tm_mday'], $tm['tm_year'] + 1900 );
183 - wfDebug( __METHOD__ . ": $thumbName is dated $thumbGMT\n" );
184 - if ( gmdate( 'YmdHis', $thumbGMT ) >= $wgThumbnailEpoch ) {
185 -
186 - return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
187 - }
188 - }
189 - $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
190 -
191 - // Ignore errors if requested
192 - if ( !$thumb ) {
193 - $thumb = null;
194 - } elseif ( $thumb->isError() ) {
195 - $this->lastError = $thumb->toText();
196 - if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
197 - $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
198 - }
199 - }
200 -
201 - // what if they didn't actually write out a thumbnail? Check the file size.
202 - if ( $thumb && file_exists( $thumbPath ) && filesize( $thumbPath ) ) {
203 - // Store the thumbnail into Swift, but in the thumb version of the container.
204 - wfDebug( __METHOD__ . ': creating thumb ' . $this->getRel() . "/$thumbName\n" );
205 - $this->repo->write_swift_object( $thumbPath, $container, $this->getRel() . "/$thumbName" );
206 - // php-cloudfiles throws exceptions, so failure never gets here.
207 - }
208 -
209 - // Clean up temporary data, if it exists.
210 - if ( file_exists( $thumbPath ) ) {
211 - wfSuppressWarnings();
212 - unlink( $thumbPath );
213 - wfRestoreWarnings();
214 - }
215 -
216 - return $thumb;
217 - }
218 -
219 - /** transform inherited */
220 -
221 - /**
222 - * We have nothing to do here.
223 - */
224 - function migrateThumbFile( $thumbName ) {
225 - return;
226 - }
227 - /**
228 - * Get the public root directory of the repository.
229 - */
230 - protected function getRootDirectory() {
231 - throw new MWException( __METHOD__ . ': not implemented' );
232 - }
233 -
234 - /** getHandler inherited */
235 - /** iconThumb inherited */
236 - /** getLastError inherited */
237 -
238 - /**
239 - * Get all thumbnail names previously generated for this file
240 - * @param $archiveName string: the article name for the archived file (if archived).
241 - * @return a list of files, the first entry of which is the directory name (if applicable).
242 - */
243 - function getThumbnails( $archiveName = false ) {
244 - $this->load();
245 -
246 - if ( $archiveName ) {
247 - $prefix = $this->getArchiveRel( $archiveName );
248 - } else {
249 - $prefix = $this->getRel();
250 - }
251 - $conn = $this->repo->connect();
252 - $container = $this->repo->get_container( $conn, $this->repo->container . '-thumb' );
253 - $files = $container->list_objects( 0, NULL, $prefix );
254 - array_unshift( $files, 'unused' ); # return an unused $dir.
255 - return $files;
256 - }
257 -
258 - /**
259 - * Delete cached transformed files
260 - * @param $dir string Should always be the 'unused' we specified earlier.
261 - * @param $files array of strings listing the thumbs to be deleted.
262 - */
263 - function purgeThumbList( $dir, $files ) {
264 - global $wgExcludeFromThumbnailPurge;
265 -
266 - $conn = $this->repo->connect();
267 - $container = $this->repo->get_container( $conn, $this->repo->container . '-thumb' );
268 - foreach ( $files as $file ) {
269 - // Only remove files not in the $wgExcludeFromThumbnailPurge configuration variable
270 - $ext = pathinfo( $file, PATHINFO_EXTENSION );
271 - if ( in_array( $ext, $wgExcludeFromThumbnailPurge ) ) {
272 - continue;
273 - }
274 -
275 - wfDebug( __METHOD__ . ' deleting ' . $container->name . "/$file\n" );
276 - $this->repo->swift_delete( $container, $file );
277 - }
278 - }
279 -
280 -} // SwiftFile class
281 -
282 -# ------------------------------------------------------------------------------
283 -
284 -/**
285 - * Repository that stores files in Swift and registers them
286 - * in the wiki's own database.
287 - *
288 - * @file
289 - * @ingroup FileRepo
290 - */
291 -
292 -class SwiftRepo extends LocalRepo {
293 - // The public interface to SwiftFile is through SwiftRepo's findFile and
294 - // newFile. They call into the repo's NewFile and FindFile, which call
295 - // one of these factories to create the File object.
296 - var $fileFactory = array( 'SwiftFile', 'newFromTitle' );
297 - var $fileFactoryKey = array( 'SwiftFile', 'newFromKey' );
298 - var $fileFromRowFactory = array( 'SwiftFile', 'newFromRow' );
299 - var $oldFileFactory = array( 'OldSwiftFile', 'newFromTitle' );
300 - var $oldFileFactoryKey = array( 'OldSwiftFile', 'newFromKey' );
301 - var $oldFileFromRowFactory = array( 'OldSwiftFile', 'newFromRow' );
302 -
303 - function __construct( $info ) {
304 - // We don't call parent::_construct because it requires $this->directory,
305 - // which doesn't exist in Swift.
306 - FileRepo::__construct( $info );
307 -
308 - // Required settings
309 - $this->url = $info['url'];
310 -
311 - // Optional settings
312 - $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
313 - $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
314 - $info['deletedHashLevels'] : $this->hashLevels;
315 -
316 - // This relationship is also hard-coded in rewrite.py, another part of this
317 - // extension. If you want to change this here, you might have to change it
318 - // there, too.
319 - $this->thumbUrl = "{$this->url}/thumb";
320 -
321 - // we don't have directories
322 - $this->deletedDir = false;
323 -
324 - // Required settings
325 - $this->swiftuser = $info['user'];
326 - $this->swiftkey = $info['key'];
327 - $this->authurl = $info['authurl'];
328 - $this->container = $info['container'];
329 - }
330 -
331 - /**
332 - * Get a connection to the swift proxy.
333 - *
334 - * @return CF_Connection
335 - */
336 - function connect() {
337 - $auth = new CF_Authentication( $this->swiftuser, $this->swiftkey, NULL, $this->authurl );
338 - try {
339 - $auth->authenticate();
340 - } catch ( AuthenticationException $e ) {
341 - throw new MWException( "We can't authenticate ourselves." );
342 - # } catch (InvalidResponseException $e) {
343 - # throw new MWException( __METHOD__ . "unexpected response '$e'" );
344 - }
345 - return new CF_Connection( $auth );
346 - }
347 -
348 - /**
349 - * Given a connection and container name, return the container.
350 - * We KNOW the container should exist, so puke if it doesn't.
351 - *
352 - * @param $conn CF_Connection
353 - *
354 - * @return CF_Container
355 - */
356 - function get_container( $conn, $cont ) {
357 - try {
358 - return $conn->get_container( $cont );
359 - } catch ( NoSuchContainerException $e ) {
360 - throw new MWException( "A container we thought existed, doesn't." );
361 - # } catch (InvalidResponseException $e) {
362 - # throw new MWException( __METHOD__ . "unexpected response '$e'" );
363 - }
364 - }
365 -
366 - /**
367 - * Given a filename, container, and object name, write the file into the object.
368 - * None of these error conditions are recoverable by the user, so we just dump
369 - * an Internal Error on them.
370 - *
371 - * @return CF_Container
372 - */
373 - function write_swift_object( $srcPath, $dstc, $dstRel ) {
374 - try {
375 - $obj = $dstc->create_object( $dstRel );
376 - $obj->load_from_filename( $srcPath, True );
377 - } catch ( SyntaxException $e ) {
378 - throw new MWException( 'missing required parameters' );
379 - } catch ( BadContentTypeException $e ) {
380 - throw new MWException( 'No Content-Type was/could be set' );
381 - # } catch (InvalidResponseException $e) {
382 - # throw new MWException( __METHOD__ . "unexpected response '$e'" );
383 - } catch ( IOException $e ) {
384 - throw new MWException( "error opening file '$e'" );
385 - }
386 - }
387 -
388 - /**
389 - * Given a container and object name, delete the object.
390 - * None of these error conditions are recoverable by the user, so we just dump
391 - * an Internal Error on them.
392 - */
393 - function swift_delete( $container, $rel ) {
394 - try {
395 - $container->delete_object( $rel );
396 - } catch ( SyntaxException $e ) {
397 - throw new MWException( "Swift object name not well-formed: '$e'" );
398 - } catch ( NoSuchObjectException $e ) {
399 - throw new MWException( "Swift object we are trying to delete does not exist: '$e'" );
400 - # } catch (InvalidResponseException $e) {
401 - # throw new MWException( "unexpected response '$e'" );
402 - }
403 - }
404 -
405 - /**
406 - * Store a batch of files
407 - *
408 - * @param $triplets Array: (src,zone,dest) triplets as per store()
409 - * @param $flags Integer: bitwise combination of the following flags:
410 - * self::DELETE_SOURCE Delete the source file after upload
411 - * self::OVERWRITE Overwrite an existing destination file instead of failing
412 - * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
413 - * same contents as the source
414 - * @return $status
415 - */
416 - function storeBatch( $triplets, $flags = 0 ) {
417 - wfDebug( __METHOD__ . ': Storing ' . count( $triplets ) .
418 - " triplets; flags: {$flags}\n" );
419 -
420 - $status = $this->newGood();
421 -
422 - // Execute the store operation for each triplet
423 - $conn = $this->connect();
424 -
425 - foreach ( $triplets as $i => $triplet ) {
426 - list( $srcPath, $dstZone, $dstRel ) = $triplet;
427 -
428 - wfDebug( __METHOD__ . ": Storing $srcPath into $dstZone::$dstRel\n" );
429 -
430 - // Point to the container.
431 - $dstContainer = $this->getZoneContainer( $dstZone );
432 - $dstc = $this->get_container( $conn, $dstContainer );
433 -
434 - $good = true;
435 -
436 - // Where are we copying this from?
437 - if ( self::isVirtualUrl( $srcPath ) ) {
438 - $src = $this->getContainerRel( $srcPath );
439 - list ( $srcContainer, $srcRel ) = $src;
440 - $srcc = $this->get_container( $conn, $srcContainer );
441 -
442 - // See if we're not supposed to overwrite an existing file.
443 - if ( !( $flags & self::OVERWRITE ) ) {
444 - // does it exist?
445 - try {
446 - $objd = $dstc->get_object( $dstRel );
447 - // and if it does, are we allowed to overwrite it?
448 - if ( $flags & self::OVERWRITE_SAME ) {
449 - $objs = $srcc->get_object( $srcRel );
450 - if ( $objd->getETag() != $objs->getETag() ) {
451 - $status->fatal( 'fileexistserror', $dstRel );
452 - $good = false;
453 - }
454 - } else {
455 - $status->fatal( 'fileexistserror', $dstRel );
456 - $good = false;
457 - }
458 - $exists = true;
459 - } catch ( NoSuchObjectException $e ) {
460 - $exists = false;
461 - }
462 - }
463 -
464 - if ( $good ) {
465 - try {
466 - $this->swiftcopy( $srcc, $srcRel, $dstc, $dstRel );
467 - } catch ( InvalidResponseException $e ) {
468 - $status->error( 'filecopyerror', $srcPath, "{$dstc->name}/$dstRel" );
469 - $good = false;
470 - }
471 - if ( $flags & self::DELETE_SOURCE ) {
472 - $this->swift_delete( $srcc, $srcRel );
473 - }
474 - }
475 - } else {
476 - // See if we're not supposed to overwrite an existing file.
477 - if ( !( $flags & self::OVERWRITE ) ) {
478 - // does it exist?
479 - try {
480 - $objd = $dstc->get_object( $dstRel );
481 - // and if it does, are we allowed to overwrite it?
482 - if ( $flags & self::OVERWRITE_SAME ) {
483 - if ( $objd->getETag() != md5_file( $srcPath ) ) {
484 - $status->fatal( 'fileexistserror', $dstRel );
485 - $good = false;
486 - }
487 - } else {
488 - $status->fatal( 'fileexistserror', $dstRel );
489 - $good = false;
490 - }
491 - $exists = true;
492 - } catch ( NoSuchObjectException $e ) {
493 - $exists = false;
494 - }
495 - }
496 - if ( $good ) {
497 - wfDebug( __METHOD__ . ": Writing $srcPath to {$dstc->name}/$dstRel\n" );
498 - try {
499 - $this->write_swift_object( $srcPath, $dstc, $dstRel );
500 - } catch ( InvalidResponseException $e ) {
501 - $status->error( 'filecopyerror', $srcPath, "{$dstc->name}/$dstRel" );
502 - $good = false;
503 - }
504 - if ( $flags & self::DELETE_SOURCE ) {
505 - unlink ( $srcPath );
506 - }
507 - }
508 - }
509 - if ( $good ) {
510 - $status->successCount++;
511 - } else {
512 - $status->failCount++;
513 - }
514 - $status->success[$i] = $good;
515 - }
516 - return $status;
517 - }
518 -
519 - /**
520 - * Append the contents of the source path to the given file, OR queue
521 - * the appending operation in anticipation of a later appendFinish() call.
522 - * @param $srcPath String: location of the source file
523 - * @param $toAppendPath String: path to append to.
524 - * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate
525 - * that the source file should be deleted if possible
526 - * @return mixed Status or false
527 - */
528 -
529 - function append( $srcPath, $toAppendPath, $flags = 0 ) {
530 - // Count the number of files whose names start with $toAppendPath
531 - $conn = $this->connect();
532 - $container = $this->repo->get_container( $conn, $this->repo->container . "-temp" );
533 - $nextone = count( $container->list_objects( 0, NULL, $srcPath ) );
534 -
535 - // Do the append to the next name
536 - $status = $this->store( $srcPath, 'temp', sprintf( "%s.%05d", $toAppendPath, $nextone ) );
537 -
538 - if ( $flags & self::DELETE_SOURCE ) {
539 - unlink( $srcPath );
540 - }
541 -
542 - return $status;
543 - }
544 - /**
545 - * Finish the append operation.
546 - * @param $toAppendPath String: path to append to.
547 - */
548 - function appendFinish( $toAppendPath ) {
549 - $conn = $this->connect();
550 - $container = $this->repo->get_container( $conn, $this->repo->container . '-temp' );
551 - $parts = $container->list_objects( 0, NULL, $toAppendPath );
552 - // list_objects() returns a sorted list.
553 -
554 - // FIXME probably want to put this into a different container.
555 - $biggie = $container->create_object( $toAppendPath );
556 - foreach ( $parts as $part ) {
557 - $obj = $container->get_object( $part );
558 - $biggie->write( $obj->read() );
559 - $obj = $container->delete_object( $part );
560 - }
561 - return Status::newGood();
562 - }
563 -
564 - /**
565 - * Move a group of files to the deletion archive.
566 - * If no valid deletion archive is configured, this may either delete the
567 - * file or throw an exception, depending on the preference of the repository.
568 - *
569 - * @param $sourceDestPairs Array of source/destination pairs. Each element
570 - * is a two-element array containing the source file path relative to the
571 - * public root in the first element, and the archive file path relative
572 - * to the deleted zone root in the second element.
573 - * @return FileRepoStatus
574 - */
575 - function deleteBatch( $sourceDestPairs ) {
576 - wfDebug( __METHOD__ . ' deleting ' . var_export( $sourceDestPairs, true ) . '\n' );
577 -
578 - /**
579 - * Move the files
580 - */
581 - $triplets = array();
582 - foreach ( $sourceDestPairs as $pair ) {
583 - list( $srcRel, $archiveRel ) = $pair;
584 -
585 - $triplets[] = array( "mwrepo://{$this->name}/public/$srcRel", 'deleted', $archiveRel );
586 -
587 - }
588 - $status = $this->storeBatch( $triplets, FileRepo::OVERWRITE_SAME | FileRepo::DELETE_SOURCE );
589 - return $status;
590 - }
591 -
592 -
593 - function newFromArchiveName( $title, $archiveName ) {
594 - return OldSwiftFile::newFromArchiveName( $title, $this, $archiveName );
595 - }
596 -
597 - /**
598 - * Checks existence of specified array of files.
599 - *
600 - * @param $files Array: URLs of files to check
601 - * @param $flags Integer: bitwise combination of the following flags:
602 - * self::FILES_ONLY Mark file as existing only if it is a file (not directory)
603 - * @return Either array of files and existence flags, or false
604 - */
605 - function fileExistsBatch( $files, $flags = 0 ) {
606 - if ( $flags != self::FILES_ONLY ) {
607 - // we ONLY support when $flags & self::FILES_ONLY is set!
608 - throw new MWException( "Swift Media Store doesn't have directories" );
609 - }
610 - $result = array();
611 - $conn = $this->connect();
612 -
613 - foreach ( $files as $key => $file ) {
614 - if ( !self::isVirtualUrl( $file ) ) {
615 - throw new MWException( __METHOD__ . " requires a virtual URL, not '$file'" );
616 - }
617 - $rvu = $this->getContainerRel( $file );
618 - list ( $cont, $rel ) = $rvu;
619 - $container = $this->get_container( $conn, $cont );
620 - try {
621 - $obj = $container->get_object( $rel );
622 - $result[$key] = true;
623 - } catch ( NoSuchObjectException $e ) {
624 - $result[$key] = false;
625 - }
626 - }
627 -
628 - return $result;
629 - }
630 -
631 - // FIXME: do we really need to reject empty titles?
632 - function newFile( $title, $time = false ) {
633 - if ( empty( $title ) ) {
634 - return null;
635 - }
636 - return parent::newFile( $title, $time );
637 - }
638 -
639 - /**
640 - * Copy a file from one place to another place in the same container
641 - * @param $srcContainer CF_Container
642 - * @param $srcRel String: relative path to the source file.
643 - * @param $dstContainer CF_Container
644 - * @param $dstRel String: relative path to the destination.
645 - */
646 - protected function swiftcopy( $srcContainer, $srcRel, $dstContainer, $dstRel ) {
647 - // The destination must exist already.
648 - $obj = $dstContainer->create_object( $dstRel );
649 - $obj->content_type = 'text/plain';
650 -
651 - try {
652 - $obj->write( '.' );
653 - } catch ( SyntaxException $e ) {
654 - throw new MWException( "Write failed: $e" );
655 - } catch ( BadContentTypeException $e ) {
656 - throw new MWException( "Missing Content-Type: $e" );
657 - } catch ( MisMatchedChecksumException $e ) {
658 - throw new MWException( __METHOD__ . "should not happen: '$e'" );
659 - }
660 -
661 - try {
662 - $obj = $dstContainer->get_object( $dstRel );
663 - } catch ( NoSuchObjectException $e ) {
664 - throw new MWException( 'The object we just created does not exist: ' . $dstContainer->name . "/$dstRel: $e" );
665 - }
666 -
667 - try {
668 - $srcObj = $srcContainer->get_object( $srcRel );
669 - } catch ( NoSuchObjectException $e ) {
670 - throw new MWException( 'Source file does not exist: ' . $srcContainer->name . "/$srcRel: $e" );
671 - }
672 -
673 - wfDebug( __METHOD__ . ' copying to ' . $dstContainer->name . "/$dstRel from " . $srcContainer->name . "/$srcRel\n" );
674 -
675 - try {
676 - $dstContainer->copy_object_from( $srcObj, $srcContainer, $dstRel );
677 - } catch ( SyntaxException $e ) {
678 - throw new MWException( 'Source file does not exist: ' . $srcContainer->name . "/$srcRel: $e" );
679 - } catch ( MisMatchedChecksumException $e ) {
680 - throw new MWException( "Checksums do not match: $e" );
681 - }
682 - }
683 -
684 - /**
685 - * Publish a batch of files
686 - * @param $triplets Array: (source,dest,archive) triplets as per publish()
687 - * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate
688 - * that the source files should be deleted if possible
689 - */
690 - function publishBatch( $triplets, $flags = 0 ) {
691 -
692 - # paranoia
693 - $status = $this->newGood( array() );
694 - foreach ( $triplets as $triplet ) {
695 - list( $srcPath, $dstRel, $archiveRel ) = $triplet;
696 -
697 - if ( !$this->validateFilename( $dstRel ) ) {
698 - throw new MWException( "Validation error in $dstRel" );
699 - }
700 - if ( !$this->validateFilename( $archiveRel ) ) {
701 - throw new MWException( "Validation error in $archiveRel" );
702 - }
703 - }
704 -
705 - if ( !$status->ok ) {
706 - return $status;
707 - }
708 -
709 - try {
710 - $conn = $this->connect();
711 - $container = $this->get_container( $conn, $this->container );
712 - } catch ( InvalidResponseException $e ) {
713 - $status->fatal( "Unexpected Swift response: '$e'" );
714 - }
715 -
716 - if ( !$status->ok ) {
717 - return $status;
718 - }
719 -
720 - foreach ( $triplets as $i => $triplet ) {
721 - list( $srcPath, $dstRel, $archiveRel ) = $triplet;
722 -
723 - // Archive destination file if it exists
724 - try {
725 - $pic = $container->get_object( $dstRel );
726 - } catch ( InvalidResponseException $e ) {
727 - $status->error( "Unexpected Swift response: '$e'" );
728 - $status->failCount++;
729 - continue;
730 - } catch ( NoSuchObjectException $e ) {
731 - $pic = NULL;
732 - }
733 -
734 - if ( $pic ) {
735 - $this->swiftcopy( $container, $dstRel, $container, $archiveRel );
736 - wfDebug( __METHOD__ . ": moved file $dstRel to $archiveRel\n" );
737 - $status->value[$i] = 'archived';
738 - } else {
739 - $status->value[$i] = 'new';
740 - }
741 -
742 - $good = true;
743 - try {
744 - // Where are we copying this from?
745 - if ( self::isVirtualUrl( $srcPath ) ) {
746 - $src = $this->getContainerRel( $srcPath );
747 - list ( $srcContainer, $srcRel ) = $src;
748 - $srcc = $this->get_container( $conn, $srcContainer );
749 -
750 - $this->swiftcopy( $srcc, $srcRel, $container, $dstRel );
751 - if ( $flags & self::DELETE_SOURCE ) {
752 - $this->swift_delete( $srcc, $srcRel );
753 - }
754 - } else {
755 - $this->write_swift_object( $srcPath, $container, $dstRel );
756 - // php-cloudfiles throws exceptions, so failure never gets here.
757 - if ( $flags & self::DELETE_SOURCE ) {
758 - unlink ( $srcPath );
759 - }
760 - }
761 - } catch ( InvalidResponseException $e ) {
762 - $status->error( "Unexpected Swift response: '$e'" );
763 - $good = false;
764 - }
765 -
766 - if ( $good ) {
767 - $status->successCount++;
768 - wfDebug( __METHOD__ . ": wrote tempfile $srcPath to $dstRel\n" );
769 - } else {
770 - $status->failCount++;
771 - }
772 - }
773 - return $status;
774 - }
775 -
776 - /**
777 - * Deletes a batch of files. Each file can be a (zone, rel) pairs, a
778 - * virtual url or a real path. It will try to delete each file, but
779 - * ignores any errors that may occur
780 - *
781 - * @param $pairs array List of files to delete
782 - */
783 - function cleanupBatch( $files ) {
784 - $conn = $this->connect();
785 - foreach ( $files as $file ) {
786 - if ( is_array( $file ) ) {
787 - // This is a pair, extract it
788 - list( $cont, $rel ) = $file;
789 - } else {
790 - if ( self::isVirtualUrl( $file ) ) {
791 - // This is a virtual url, resolve it
792 - $path = $this->getContainerRel( $file );
793 - list( $cont, $rel ) = $path;
794 - } else {
795 - // FIXME: This is a full file name
796 - throw new MWException( __METHOD__ . ": $file needs an unlink()" );
797 - }
798 - }
799 -
800 - wfDebug( __METHOD__ . ": $cont/$rel\n" );
801 - $container = $this->get_container( $conn, $cont );
802 - $this->swift_delete( $container, $rel );
803 - }
804 - }
805 -
806 - /**
807 - * Delete files in the deleted directory if they are not referenced in the
808 - * filearchive table. This needs to be done in the repo because it needs to
809 - * interleave database locks with file operations, which is potentially a
810 - * remote operation.
811 - * @return FileRepoStatus
812 - */
813 - function cleanupDeletedBatch( $storageKeys ) {
814 - $conn = $this->connect();
815 - $cont = $this->getZoneContainer( 'deleted' );
816 - $container = $this->get_container( $conn, $cont );
817 -
818 - $dbw = $this->getMasterDB();
819 - $status = $this->newGood();
820 - $storageKeys = array_unique( $storageKeys );
821 - foreach ( $storageKeys as $key ) {
822 - $hashPath = $this->getDeletedHashPath( $key );
823 - $rel = "$hashPath$key";
824 - $dbw->begin();
825 - $inuse = $dbw->selectField( 'filearchive', '1',
826 - array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
827 - __METHOD__, array( 'FOR UPDATE' ) );
828 - if ( !$inuse ) {
829 - $sha1 = self::getHashFromKey( $key );
830 - $ext = substr( $key, strcspn( $key, '.' ) + 1 );
831 - $ext = File::normalizeExtension( $ext );
832 - $inuse = $dbw->selectField( 'oldimage', '1',
833 - array( 'oi_sha1' => $sha1,
834 - 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ),
835 - $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
836 - __METHOD__, array( 'FOR UPDATE' ) );
837 - }
838 - if ( !$inuse ) {
839 - wfDebug( __METHOD__ . ": deleting $key\n" );
840 - $this->swift_delete( $container, $rel );
841 - } else {
842 - wfDebug( __METHOD__ . ": $key still in use\n" );
843 - $status->successCount++;
844 - }
845 - $dbw->commit();
846 - }
847 - return $status;
848 - }
849 -
850 - /**
851 - * Makes no sense in our context -- don't let anybody call it.
852 - */
853 - function getZonePath( $zone ) {
854 - throw new MWException( __METHOD__ . ': not implemented' );
855 - }
856 -
857 - /**
858 - * Get the Swift container corresponding to one of the three basic zones
859 - */
860 - public function getZoneContainer( $zone ) {
861 - switch ( $zone ) {
862 - case 'public':
863 - return $this->container;
864 - case 'temp':
865 - return $this->container . '-temp';
866 - case 'deleted':
867 - return $this->container . '-deleted';
868 - case 'thumb':
869 - return $this->container . '-thumb';
870 - default:
871 - return false;
872 - }
873 - }
874 -
875 - /**
876 - * Get a local path corresponding to a virtual URL
877 - */
878 - protected function getContainerRel( $url ) {
879 - if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
880 - throw new MWException( __METHOD__ . ': unknown protocol' );
881 - }
882 -
883 - $bits = explode( '/', substr( $url, 9 ), 3 );
884 - if ( count( $bits ) != 3 ) {
885 - throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" );
886 - }
887 - list( $repo, $zone, $rel ) = $bits;
888 - if ( $repo !== $this->name ) {
889 - throw new MWException( __METHOD__ . ': fetching from a foreign repo is not supported' );
890 - }
891 - $container = $this->getZoneContainer( $zone );
892 - if ( $container === false ) {
893 - throw new MWException( __METHOD__ . ": invalid zone: $zone" );
894 - }
895 - return array( $container, rawurldecode( $rel ) );
896 - }
897 -
898 - /**
899 - * Remove a temporary file or mark it for garbage collection
900 - * @param $virtualUrl String: the virtual URL returned by storeTemp
901 - * @return Boolean: true on success, false on failure
902 - */
903 - function freeTemp( $virtualUrl ) {
904 - $temp = "mwrepo://{$this->name}/temp";
905 - if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
906 - wfDebug( __METHOD__ . ": Invalid virtual URL\n" );
907 - return false;
908 - }
909 - $path = $this->getContainerRel( $virtualUrl );
910 - list ( $c, $r ) = $path;
911 - $conn = $this->connect();
912 - $container = $this->get_container( $conn, $c );
913 - $this->swift_delete( $container, $r );
914 - }
915 -
916 - /**
917 - * Called from elsewhere to turn a virtual URL into a path.
918 - * Make sure you delete this file after you've used it!!
919 - */
920 - function resolveVirtualUrl( $url ) {
921 - $path = $this->getContainerRel( $url );
922 - list( $c, $r ) = $path;
923 - return $this->getLocalCopy( $c, $r );
924 - }
925 -
926 -
927 - /**
928 - * Given a container and relative path, return an absolute path pointing at a
929 - * copy of the file MUST delete the produced file, or else store it in
930 - * SwiftFile->tempPath so it will be deleted when the object goes out of
931 - * scope.
932 - */
933 - function getLocalCopy( $container, $rel, $prefix = 'swift_in_' ) {
934 -
935 - // get a temporary place to put the original.
936 - $tempPath = tempnam( wfTempDir(), $prefix );
937 - unlink( $tempPath );
938 - $tempPath .= '.' . pathinfo( $rel, PATHINFO_EXTENSION );
939 -
940 - /* Fetch the image out of Swift */
941 - $conn = $this->connect();
942 - $cont = $this->get_container( $conn, $container );
943 -
944 - try {
945 - $obj = $cont->get_object( $rel );
946 - } catch ( NoSuchObjectException $e ) {
947 - throw new MWException( "Unable to open original file at $container/$rel" );
948 - }
949 -
950 - wfDebug( __METHOD__ . " writing to $tempPath\n" );
951 - try {
952 - $obj->save_to_filename( $tempPath );
953 - } catch ( IOException $e ) {
954 - throw new MWException( __METHOD__ . ": error opening '$e'" );
955 - } catch ( InvalidResponseException $e ) {
956 - throw new MWException( __METHOD__ . "unexpected response '$e'" );
957 - }
958 -
959 - return $tempPath;
960 - }
961 -
962 -
963 - /**
964 - * Get properties of a file with a given virtual URL
965 - * The virtual URL must refer to this repo
966 - */
967 - function getFileProps( $virtualUrl ) {
968 - $path = $this->resolveVirtualUrl( $virtualUrl );
969 - $ret = File::getPropsFromPath( $path );
970 - unlink( $path );
971 - return $ret;
972 - }
973 -
974 -
975 - /**
976 - * Get an UploadStash associated with this repo.
977 - *
978 - * @return UploadStash
979 - */
980 - function getUploadStash() {
981 - return new SwiftStash( $this );
982 - }
983 -}
984 -
985 -class SwiftStash extends UploadStash {
986 - /**
987 - * Wrapper function for subclassing.
988 - */
989 - protected function newFile( $path, $key, $data ) {
990 - wfDebug( __METHOD__ . ": deleting $key\n" );
991 - return new SwiftStashFile( $this, $this->repo, $path, $key, $data );
992 - }
993 -
994 -}
995 -
996 -class SwiftStashFile extends UploadStashFile {
997 - // public function __construct( $stash, $repo, $path, $key, $data ) {
998 - // // We don't call parent:: because UploadStashFile expects to be able to call $this->resolveURL() and get a pathname.
999 - // $this->sessionStash = $stash;
1000 - // $this->sessionKey = $key;
1001 - // $this->sessionData = $data;
1002 - // wfDebug( __METHOD__ . ": ($stash, $repo, $path, $key, $data)\n" );
1003 -
1004 - // UnregisteredLocalFile::__construct( false, $repo, $path, false );
1005 - // $this->name = basename( $this->path );
1006 -
1007 - // }
1008 -
1009 - // function getPath() {
1010 - // }
1011 -}
1012 -
1013 -/**
1014 - * Old file in the in the oldimage table
1015 - *
1016 - * @file
1017 - * @ingroup FileRepo
1018 - */
1019 -
1020 -/**
1021 - * Class to represent a file in the oldimage table
1022 - *
1023 - * @ingroup FileRepo
1024 - */
1025 -class OldSwiftFile extends SwiftFile {
1026 - var $requestedTime, $archive_name;
1027 -
1028 - const CACHE_VERSION = 1;
1029 - const MAX_CACHE_ROWS = 20;
1030 -
1031 - static function newFromTitle( $title, $repo, $time = null ) {
1032 - # The null default value is only here to avoid an E_STRICT
1033 - if ( $time === null )
1034 - throw new MWException( __METHOD__ . ' got null for $time parameter' );
1035 - return new self( $title, $repo, $time, null );
1036 - }
1037 -
1038 - static function newFromArchiveName( $title, $repo, $archiveName ) {
1039 - return new self( $title, $repo, null, $archiveName );
1040 - }
1041 -
1042 - static function newFromRow( $row, $repo ) {
1043 - $title = Title::makeTitle( NS_FILE, $row->oi_name );
1044 - $file = new self( $title, $repo, null, $row->oi_archive_name );
1045 - $file->loadFromRow( $row, 'oi_' );
1046 - return $file;
1047 - }
1048 -
1049 - /**
1050 - * @static
1051 - * @param $sha1
1052 - * @param $repo LocalRepo
1053 - * @param bool $timestamp
1054 - * @return bool|OldLocalFile
1055 - */
1056 - static function newFromKey( $sha1, $repo, $timestamp = false ) {
1057 - $conds = array( 'oi_sha1' => $sha1 );
1058 - if ( $timestamp ) {
1059 - $conds['oi_timestamp'] = $timestamp;
1060 - }
1061 - $dbr = $repo->getSlaveDB();
1062 - $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ );
1063 - if ( $row ) {
1064 - return self::newFromRow( $row, $repo );
1065 - } else {
1066 - return false;
1067 - }
1068 - }
1069 -
1070 - /**
1071 - * Fields in the oldimage table
1072 - */
1073 - static function selectFields() {
1074 - return array(
1075 - 'oi_name',
1076 - 'oi_archive_name',
1077 - 'oi_size',
1078 - 'oi_width',
1079 - 'oi_height',
1080 - 'oi_metadata',
1081 - 'oi_bits',
1082 - 'oi_media_type',
1083 - 'oi_major_mime',
1084 - 'oi_minor_mime',
1085 - 'oi_description',
1086 - 'oi_user',
1087 - 'oi_user_text',
1088 - 'oi_timestamp',
1089 - 'oi_deleted',
1090 - 'oi_sha1',
1091 - );
1092 - }
1093 -
1094 - /**
1095 - * @param $title Title
1096 - * @param $repo FileRepo
1097 - * @param $time String: timestamp or null to load by archive name
1098 - * @param $archiveName String: archive name or null to load by timestamp
1099 - */
1100 - function __construct( $title, $repo, $time, $archiveName ) {
1101 - parent::__construct( $title, $repo );
1102 - $this->requestedTime = $time;
1103 - $this->archive_name = $archiveName;
1104 - if ( is_null( $time ) && is_null( $archiveName ) ) {
1105 - throw new MWException( __METHOD__ . ': must specify at least one of $time or $archiveName' );
1106 - }
1107 - }
1108 -
1109 - function getCacheKey() {
1110 - return false;
1111 - }
1112 -
1113 - function getArchiveName() {
1114 - if ( !isset( $this->archive_name ) ) {
1115 - $this->load();
1116 - }
1117 - return $this->archive_name;
1118 - }
1119 -
1120 - function isOld() {
1121 - return true;
1122 - }
1123 -
1124 - function isVisible() {
1125 - return $this->exists() && !$this->isDeleted( File::DELETED_FILE );
1126 - }
1127 -
1128 - function loadFromDB() {
1129 - wfProfileIn( __METHOD__ );
1130 - $this->dataLoaded = true;
1131 - $dbr = $this->repo->getSlaveDB();
1132 - $conds = array( 'oi_name' => $this->getName() );
1133 - if ( is_null( $this->requestedTime ) ) {
1134 - $conds['oi_archive_name'] = $this->archive_name;
1135 - } else {
1136 - $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) );
1137 - }
1138 - $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
1139 - $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
1140 - if ( $row ) {
1141 - $this->loadFromRow( $row, 'oi_' );
1142 - } else {
1143 - $this->fileExists = false;
1144 - }
1145 - wfProfileOut( __METHOD__ );
1146 - }
1147 -
1148 - function getCacheFields( $prefix = 'img_' ) {
1149 - $fields = parent::getCacheFields( $prefix );
1150 - $fields[] = $prefix . 'archive_name';
1151 - $fields[] = $prefix . 'deleted';
1152 - return $fields;
1153 - }
1154 -
1155 - function getRel() {
1156 - return 'archive/' . $this->getHashPath() . $this->getArchiveName();
1157 - }
1158 -
1159 - function getUrlRel() {
1160 - return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() );
1161 - }
1162 -
1163 - function upgradeRow() {
1164 - wfProfileIn( __METHOD__ );
1165 - $this->loadFromFile();
1166 -
1167 - # Don't destroy file info of missing files
1168 - if ( !$this->fileExists ) {
1169 - wfDebug( __METHOD__ . ': file does not exist, aborting\n' );
1170 - wfProfileOut( __METHOD__ );
1171 - return;
1172 - }
1173 -
1174 - $dbw = $this->repo->getMasterDB();
1175 - list( $major, $minor ) = self::splitMime( $this->mime );
1176 -
1177 - wfDebug( __METHOD__ . ': upgrading ' . $this->archive_name . ' to the current schema\n' );
1178 - $dbw->update( 'oldimage',
1179 - array(
1180 - 'oi_width' => $this->width,
1181 - 'oi_height' => $this->height,
1182 - 'oi_bits' => $this->bits,
1183 - 'oi_media_type' => $this->media_type,
1184 - 'oi_major_mime' => $major,
1185 - 'oi_minor_mime' => $minor,
1186 - 'oi_metadata' => $this->metadata,
1187 - 'oi_sha1' => $this->sha1,
1188 - ), array(
1189 - 'oi_name' => $this->getName(),
1190 - 'oi_archive_name' => $this->archive_name ),
1191 - __METHOD__
1192 - );
1193 - wfProfileOut( __METHOD__ );
1194 - }
1195 -
1196 - /**
1197 - * @param $field Integer: one of DELETED_* bitfield constants
1198 - * for file or revision rows
1199 - * @return bool
1200 - */
1201 - function isDeleted( $field ) {
1202 - $this->load();
1203 - return ( $this->deleted & $field ) == $field;
1204 - }
1205 -
1206 - /**
1207 - * Returns bitfield value
1208 - * @return int
1209 - */
1210 - function getVisibility() {
1211 - $this->load();
1212 - return (int)$this->deleted;
1213 - }
1214 -
1215 - /**
1216 - * Determine if the current user is allowed to view a particular
1217 - * field of this image file, if it's marked as deleted.
1218 - *
1219 - * @param $field Integer
1220 - * @return bool
1221 - */
1222 - function userCan( $field ) {
1223 - $this->load();
1224 - return Revision::userCanBitfield( $this->deleted, $field );
1225 - }
1226 -}
1227 -
1228 -/**
1229 - * Foreign file with an accessible MediaWiki database
1230 - *
1231 - * @ingroup FileRepo
1232 - */
1233 -class SwiftForeignDBFile extends SwiftFile {
1234 -
1235 - /**
1236 - * @param $title
1237 - * @param $repo
1238 - * @param $unused
1239 - * @return SwiftForeignDBFile
1240 - */
1241 - static function newFromTitle( $title, $repo, $unused = null ) {
1242 - return new self( $title, $repo );
1243 - }
1244 -
1245 - /**
1246 - * Create a ForeignDBFile from a title
1247 - * Do not call this except from inside a repo class.
1248 - */
1249 - static function newFromRow( $row, $repo ) {
1250 - $title = Title::makeTitle( NS_FILE, $row->img_name );
1251 - $file = new self( $title, $repo );
1252 - $file->loadFromRow( $row );
1253 - return $file;
1254 - }
1255 -
1256 - function publish( $srcPath, $flags = 0 ) {
1257 - $this->readOnlyError();
1258 - }
1259 -
1260 - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1261 - $watch = false, $timestamp = false ) {
1262 - $this->readOnlyError();
1263 - }
1264 -
1265 - function restore( $versions = array(), $unsuppress = false ) {
1266 - $this->readOnlyError();
1267 - }
1268 -
1269 - function delete( $reason, $suppress = false ) {
1270 - $this->readOnlyError();
1271 - }
1272 -
1273 - function move( $target ) {
1274 - $this->readOnlyError();
1275 - }
1276 -
1277 - function getDescriptionUrl() {
1278 - // Restore remote behaviour
1279 - return File::getDescriptionUrl();
1280 - }
1281 -
1282 - function getDescriptionText() {
1283 - // Restore remote behaviour
1284 - return File::getDescriptionText();
1285 - }
1286 -}
1287 -
1288 -/**
1289 - * A foreign repository with an accessible MediaWiki database
1290 - *
1291 - * @ingroup FileRepo
1292 - */
1293 -class SwiftForeignDBRepo extends SwiftRepo {
1294 - # Settings
1295 - var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags,
1296 - $tablePrefix, $hasSharedCache;
1297 -
1298 - # Other stuff
1299 - var $dbConn;
1300 - var $fileFactory = array( 'SwiftForeignDBFile', 'newFromTitle' );
1301 - var $fileFromRowFactory = array( 'SwiftForeignDBFile', 'newFromRow' );
1302 -
1303 - function __construct( $info ) {
1304 - parent::__construct( $info );
1305 - $this->dbType = $info['dbType'];
1306 - $this->dbServer = $info['dbServer'];
1307 - $this->dbUser = $info['dbUser'];
1308 - $this->dbPassword = $info['dbPassword'];
1309 - $this->dbName = $info['dbName'];
1310 - $this->dbFlags = $info['dbFlags'];
1311 - $this->tablePrefix = $info['tablePrefix'];
1312 - $this->hasSharedCache = $info['hasSharedCache'];
1313 - }
1314 -
1315 - /**
1316 - * @return DatabaseBase
1317 - */
1318 - function getMasterDB() {
1319 - wfDebug( __METHOD__ . ": {$this->dbServer}\n" );
1320 - if ( !isset( $this->dbConn ) ) {
1321 - $this->dbConn = DatabaseBase::factory( $this->dbType,
1322 - array(
1323 - 'host' => $this->dbServer,
1324 - 'user' => $this->dbUser,
1325 - 'password' => $this->dbPassword,
1326 - 'dbname' => $this->dbName,
1327 - 'flags' => $this->dbFlags,
1328 - 'tablePrefix' => $this->tablePrefix
1329 - )
1330 - );
1331 - }
1332 - return $this->dbConn;
1333 - }
1334 -
1335 - /**
1336 - * @return DatabaseBase
1337 - */
1338 - function getSlaveDB() {
1339 - return $this->getMasterDB();
1340 - }
1341 -
1342 - function hasSharedCache() {
1343 - return $this->hasSharedCache;
1344 - }
1345 -
1346 - /**
1347 - * Get a key on the primary cache for this repository.
1348 - * Returns false if the repository's cache is not accessible at this site.
1349 - * The parameters are the parts of the key, as for wfMemcKey().
1350 - */
1351 - function getSharedCacheKey( /*...*/ ) {
1352 - if ( $this->hasSharedCache() ) {
1353 - $args = func_get_args();
1354 - array_unshift( $args, $this->dbName, $this->tablePrefix );
1355 - return call_user_func_array( 'wfForeignMemcKey', $args );
1356 - } else {
1357 - return false;
1358 - }
1359 - }
1360 -
1361 - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
1362 - throw new MWException( get_class( $this ) . ': write operations are not supported' );
1363 - }
1364 -
1365 - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
1366 - throw new MWException( get_class( $this ) . ': write operations are not supported' );
1367 - }
1368 -
1369 - function deleteBatch( $sourceDestPairs ) {
1370 - throw new MWException( get_class( $this ) . ': write operations are not supported' );
1371 - }
1372 -}
1373 -
1374 -/**
1375 - * A foreign repository with a MediaWiki database accessible via the configured LBFactory
1376 - *
1377 - * @file
1378 - * @ingroup FileRepo
1379 - */
1380 -
1381 -/**
1382 - * A foreign repository with a MediaWiki database accessible via the configured LBFactory
1383 - *
1384 - * @ingroup FileRepo
1385 - */
1386 -class SwiftForeignDBViaLBRepo extends SwiftRepo {
1387 - var $wiki, $dbName, $tablePrefix;
1388 - var $fileFactory = array( 'SwiftForeignDBFile', 'newFromTitle' );
1389 - var $fileFromRowFactory = array( 'SwiftForeignDBFile', 'newFromRow' );
1390 -
1391 - function __construct( $info ) {
1392 - parent::__construct( $info );
1393 - $this->wiki = $info['wiki'];
1394 - list( $this->dbName, $this->tablePrefix ) = wfSplitWikiID( $this->wiki );
1395 - $this->hasSharedCache = $info['hasSharedCache'];
1396 - }
1397 -
1398 - /**
1399 - * @return DatabaseBase
1400 - */
1401 - function getMasterDB() {
1402 - return wfGetDB( DB_MASTER, array(), $this->wiki );
1403 - }
1404 -
1405 - /**
1406 - * @return DatabaseBase
1407 - */
1408 - function getSlaveDB() {
1409 - return wfGetDB( DB_SLAVE, array(), $this->wiki );
1410 - }
1411 -
1412 - /**
1413 - * @return bool
1414 - */
1415 - function hasSharedCache() {
1416 - return $this->hasSharedCache;
1417 - }
1418 -
1419 - /**
1420 - * Get a key on the primary cache for this repository.
1421 - * Returns false if the repository's cache is not accessible at this site.
1422 - * The parameters are the parts of the key, as for wfMemcKey().
1423 - */
1424 - function getSharedCacheKey( /*...*/ ) {
1425 - if ( $this->hasSharedCache() ) {
1426 - $args = func_get_args();
1427 - array_unshift( $args, $this->wiki );
1428 - return implode( ':', $args );
1429 - } else {
1430 - return false;
1431 - }
1432 - }
1433 -
1434 - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
1435 - throw new MWException( get_class( $this ) . ': write operations are not supported' );
1436 - }
1437 -
1438 - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
1439 - throw new MWException( get_class( $this ) . ': write operations are not supported' );
1440 - }
1441 -
1442 - function deleteBatch( $fileMap ) {
1443 - throw new MWException( get_class( $this ) . ': write operations are not supported' );
1444 - }
1445 -}
Index: trunk/extensions/SwiftMedia/wmf/tests/smtest.py
@@ -0,0 +1,169 @@
 2+#!/usr/bin/python
 3+# http://www.deheus.net/petrik/blog/2005/11/20/creating-a-wikipedia-watchlist-rss-feed-with-python-and-twill/
 4+
 5+import sys, string, datetime, time, os, re, stat
 6+import twill
 7+import twill.commands as t
 8+import gd
 9+
 10+temp_html = "/tmp/wikipedia.html"
 11+rss_title = "Wikipedia watchlist"
 12+rss_link = "http://en.wikipedia.org"
 13+host = "http://ersch.wikimedia.org/"
 14+#host = "http://127.0.0.1/wiki/"
 15+
 16+def login(username, password):
 17+ t.add_extra_header("User-Agent", "python-twill-russnelson@gmail.com")
 18+
 19+ t.go(host+"index.php/Special:UserLogin")
 20+ t.fv("1", "wpName", username)
 21+ t.fv("1", "wpPassword", password)
 22+ t.submit("wpLoginAttempt")
 23+
 24+
 25+def upload_list(browser, pagename, uploads):
 26+
 27+ # get the file sizes for later comparison.
 28+ filesizes = []
 29+ for fn in uploads:
 30+ filesizes.append(os.stat(fn)[stat.ST_SIZE])
 31+ filesizes.reverse() # because they get listed newest first.
 32+
 33+ # Upload copy #1.
 34+ t.go(host+"index.php/Special:Upload")
 35+ t.formfile("1", "wpUploadFile", uploads[0])
 36+ t.fv("1", "wpDestFile", pagename)
 37+ t.fv("1", "wpUploadDescription", "Uploading %s" % pagename)
 38+ t.submit("wpUpload")
 39+
 40+ # Verify that we succeeded.
 41+ t.find("File:%s" % pagename)
 42+
 43+ for fn in uploads[1:]:
 44+ # propose that we upload a replacement
 45+ t.go(host+"index.php?title=Special:Upload&wpDestFile=%s&wpForReUpload=1" % pagename)
 46+ t.formfile("1", "wpUploadFile", fn)
 47+ t.fv("1", "wpUploadDescription", "Uploading %s as %s" % (fn, pagename))
 48+ t.submit("wpUpload")
 49+
 50+ # get the URLs for the thumbnails
 51+ urls = []
 52+ for url in re.finditer(r'<td><a href="([^"]*?)"><img alt="Thumbnail for version .*?" src="(.*?)"', browser.get_html()):
 53+ urls.append(url.group(1))
 54+ urls.append(url.group(2))
 55+
 56+ print filesizes
 57+ for i, url in enumerate(urls):
 58+ t.go(url)
 59+ if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]:
 60+ print i,len(browser.get_html()), filesizes[i / 2]
 61+ t.find("Files differ in size")
 62+ t.code("200")
 63+ t.back()
 64+
 65+ # delete all versions
 66+ t.go(host+"index.php?title=File:%s&action=delete" % pagename)
 67+ # after we get the confirmation page, commit to the action.
 68+ t.fv("1", "wpReason", "Test Deleting...")
 69+ t.submit("mw-filedelete-submit")
 70+
 71+ # make sure that we can't visit their URLs.
 72+ for i, url in enumerate(urls):
 73+ t.go(url)
 74+ if 0 and i % 2 == 1 and i > 0 and browser.get_code() == 200:
 75+ # bug 30192: the archived file's thumbnail doesn't get deleted.
 76+ print "special-casing the last URL"
 77+ continue
 78+ t.code("404")
 79+
 80+ # restore the current and archived version.
 81+ t.go(host+"index.php/Special:Undelete/File:%s" % pagename)
 82+ t.fv("1", "wpComment", "Test Restore")
 83+ t.submit("restore")
 84+
 85+ # visit the page to make sure that the thumbs get re-rendered properly.
 86+ # when we get the 404 handler working correctly, this won't be needed.
 87+ t.go(host+"index.php?title=File:%s" % pagename)
 88+
 89+ # make sure that they got restored correctly.
 90+ for i, url in enumerate(urls):
 91+ t.go(url)
 92+ if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]:
 93+ t.find("Files differ in size")
 94+ t.code("200")
 95+ t.back()
 96+
 97+ if len(uploads) != 2:
 98+ return
 99+
 100+ match = re.search(r'"([^"]+?)" title="[^"]+?">revert', browser.get_html())
 101+ if not match:
 102+ t.find('revert')
 103+ t.go(match.group(1).replace('&amp;', '&'))
 104+
 105+def make_files(pagename):
 106+ redfilename = "/tmp/Red-%s" % pagename
 107+ greenfilename = "/tmp/Green-%s" % pagename
 108+ bluefilename = "/tmp/Blue-%s" % pagename
 109+
 110+ # create a small test image.
 111+ gd.gdMaxColors = 256
 112+ i = gd.image((200,100))
 113+ black = i.colorAllocate((0,0,0))
 114+ white = i.colorAllocate((255,255,255))
 115+ red = i.colorAllocate((255,55,55))
 116+ green = i.colorAllocate((55,255,55))
 117+ blue = i.colorAllocate((55,55,255))
 118+
 119+ # now write a red version
 120+ i.rectangle((0,0),(199,99),red, red)
 121+ i.line((0,0),(199,99),black)
 122+ i.string(gd.gdFontLarge, (5,50), pagename, white)
 123+ i.writePng(redfilename)
 124+
 125+ # now write a green version
 126+ i.rectangle((0,0),(199,99),green, green)
 127+ i.line((0,0),(99,99),black)
 128+ i.string(gd.gdFontLarge, (5,50), pagename, white)
 129+ i.writePng(greenfilename)
 130+
 131+ # write a blue version
 132+ i.rectangle((0,0),(199,99),blue,blue)
 133+ i.line((0,0),(99,199),black)
 134+ i.string(gd.gdFontLarge, (5,50), pagename, white)
 135+ i.writePng(bluefilename)
 136+
 137+ # propose that we delete it (in case it exists)
 138+ t.go(host+"index.php?title=File:%s&action=delete" % pagename)
 139+ # make sure that we've NOT gotten the wrong page and HAVE gotten the right one.
 140+ t.notfind('You are about to delete the file')
 141+ t.find("could not be deleted")
 142+
 143+ return (redfilename, greenfilename, bluefilename )
 144+
 145+def main():
 146+ try:
 147+ username = sys.argv[1]
 148+ password = sys.argv[2]
 149+ except IndexError:
 150+ print "Please supply username password"
 151+ sys.exit(1)
 152+ browser = twill.get_browser()
 153+ login(username, password)
 154+
 155+ serial = time.time()
 156+ pagename = "Test-%s.png" % serial
 157+ filenames = make_files(pagename)
 158+ upload_list(browser, pagename, filenames[0:2])
 159+
 160+ # try it again with two replacement files.
 161+# pagename = "Test-%sA.png" % serial
 162+# filenames = make_files(pagename)
 163+# upload_list(browser, pagename, filenames)
 164+
 165+ t.showforms()
 166+ t.save_html("/tmp/testabcd")
 167+
 168+if __name__ == "__main__":
 169+ main()
 170+
Property changes on: trunk/extensions/SwiftMedia/wmf/tests/smtest.py
___________________________________________________________________
Added: svn:eol-style
1171 + native
Added: svn:executable
2172 + *
Index: trunk/extensions/SwiftMedia/wmf/tests/test_rewrite.py
@@ -0,0 +1,171 @@
 2+#!/usr/bin/python
 3+
 4+import unittest
 5+
 6+import webob
 7+
 8+from wmf import rewrite
 9+from wmf.client import ClientException
 10+
 11+class FakeApp(object):
 12+ def __init__(self, status, headers):
 13+ self.status = status
 14+ self.headers = headers
 15+
 16+ def __call__(self, env, start_response):
 17+ start_response(self.status, self.headers)
 18+ return "FAKE APP"
 19+
 20+def start_response(*args):
 21+ pass
 22+
 23+class TestRewrite(unittest.TestCase):
 24+
 25+ def setUp(self):
 26+ pass
 27+
 28+ account="AUTH_..."
 29+ urlbig = 'http://alsted.wikimedia.org/wikipedia/commons/a/aa/'\
 30+ 'Dzimbo_u_Beogradu_19.jpeg'
 31+ urlorig = '/v1/' + account + \
 32+ '/wikipedia%2Fcommons/a/aa/Dzimbo_u_Beogradu_19.jpeg'
 33+ thumbbig = 'http://alsted.wikimedia.org/wikipedia/commons/thumb/a/aa/'\
 34+ 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg'
 35+ url448 = '/v1/' + account + \
 36+ '/wikipedia%2Fcommons%2Fthumb/a/aa/'\
 37+ 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg'
 38+ urlaccount = 'http://alsted.wikimedia.org/' + account
 39+ contname = '/wikipedia/commons/thumb'
 40+ objname = '/a/aa/Dzimbo_u_Beogradu_19.jpeg/91px-Dzimbo_u_Beogradu_19.jpeg'
 41+ a = dict(account=account,
 42+ url="https://127.0.0.1:11000/v1.0",
 43+ login="yourlogin",
 44+ thumbhost='localhost',
 45+ user_agent='Mozilla/5.0',
 46+ key="yourkey")
 47+
 48+ def test_01(self):
 49+ """#01 Cur controller can snarf its args."""
 50+ controller = rewrite.ObjectController()
 51+ controller.do_start_response("200 Good", {"test": "testy"})
 52+ self.assertEquals(controller.response_args[0], "200 Good")
 53+ self.assertEquals(controller.response_args[1], {"test": "testy"})
 54+
 55+ def test_01a(self):
 56+ """#01a Our app calls into the FakeApp; returns its results if 200 """
 57+ app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
 58+ req = webob.Request.blank(self.urlbig,
 59+ environ={'REQUEST_METHOD': 'GET'})
 60+ controller = rewrite.ObjectController()
 61+ resp = app(req.environ, controller.do_start_response)
 62+ self.assertEquals(resp, 'FAKE APP')
 63+
 64+ def test_02(self):
 65+ """#02 Test URL rewriting for originals. """
 66+ app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
 67+ req = webob.Request.blank(self.urlbig,
 68+ environ={'REQUEST_METHOD': 'GET'})
 69+ resp = app(req.environ, start_response)
 70+ self.assertEquals(req.environ['PATH_INFO'], self.urlorig)
 71+
 72+ def test_03(self):
 73+ """#03 Test URL rewriting for thumbs. """
 74+ app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
 75+ req = webob.Request.blank(self.thumbbig,
 76+ environ={'REQUEST_METHOD': 'GET'})
 77+ resp = app(req.environ, start_response)
 78+ self.assertEquals(req.environ['PATH_INFO'], self.url448)
 79+
 80+ def test_04(self):
 81+ """#04 Test a write. Could fail if our token has gone stale"""
 82+ app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a)
 83+ req = webob.Request.blank(self.thumbbig,
 84+ environ={'REQUEST_METHOD': 'GET'})
 85+ resp = app(req.environ, start_response)
 86+ self.assertEquals(req.environ['PATH_INFO'], self.url448)
 87+ # note that we PUT this file onto the server here, even if it's already there.
 88+ datalen = 0
 89+ for data in resp:
 90+ datalen += len(data)
 91+ self.assertEquals(datalen, 51543)
 92+
 93+ def test_05(self):
 94+ """#05 Report 401 (authorization) errors"""
 95+ app = rewrite.WMFRewrite(FakeApp("401 Bad", {}),self.a)
 96+ req = webob.Request.blank(self.thumbbig,
 97+ environ={'REQUEST_METHOD': 'GET'})
 98+ resp = app(req.environ, start_response)
 99+ self.assertEquals(req.environ['PATH_INFO'], self.url448)
 100+ self.assertEquals(resp,
 101+ ['401 Unauthorized\n\nThis server could not verify that you are '\
 102+ 'authorized to access the document you requested. Either you '\
 103+ 'supplied the wrong credentials (e.g., bad password), or your '\
 104+ 'browser does not understand how to supply the credentials '\
 105+ 'required.\n\n Token may have timed out '])
 106+
 107+ def test_06(self):
 108+ """#06 Give them a bad token so that the PUT fails."""
 109+ app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a)
 110+ app.token = "HaHaYeahRight"
 111+ req = webob.Request.blank(self.thumbbig,
 112+ environ={'REQUEST_METHOD': 'GET'})
 113+ resp = app(req.environ, start_response)
 114+ self.assertEquals(req.environ['PATH_INFO'], self.url448)
 115+ # the PUT fails because we give them a bad token, but ... we should
 116+ # really silently just hand back the file.
 117+ try:
 118+ datalen = 0
 119+ for data in resp:
 120+ datalen += len(data)
 121+ except ClientException, x:
 122+ self.assertEquals(datalen, 51543)
 123+ y = "ClientException('Object PUT failed',)"
 124+ self.assertEquals(`x`, y)
 125+
 126+ def test_07(self):
 127+ """#07 Make sure that an already-authorized path goes unchanged."""
 128+ app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
 129+ url = self.urlaccount + self.contname + self.objname
 130+ req = webob.Request.blank(url, environ={'REQUEST_METHOD': 'GET'})
 131+ resp = app(req.environ, start_response)
 132+ self.assertEquals(req.url, url) # should remain unchanged
 133+ #self.assertTrue(len(app.response_args) == 0) # no args either.
 134+
 135+ def test_08(self):
 136+ """#08 Don't let them read the container"""
 137+ app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a)
 138+ req = webob.Request.blank(
 139+ 'http://alsted.wikimedia.org/wikipedia/commons/',
 140+ environ={'REQUEST_METHOD': 'GET'})
 141+ resp = app(req.environ, start_response)
 142+ self.assertEquals(resp,
 143+ ['403 Forbidden\n\nAccess was denied to this resource.\n\n '\
 144+ 'No container listing '])
 145+
 146+ # test_09 became obsolete
 147+
 148+ def test_10(self):
 149+ """#10 Trap weird-ass errors"""
 150+ app = rewrite.WMFRewrite(FakeApp("999 Unrecognized", {}),self.a)
 151+ req = webob.Request.blank("http://localhost/a/b/c",
 152+ environ={'REQUEST_METHOD': 'GET'})
 153+ resp = app(req.environ, start_response)
 154+ self.assertEquals(resp,
 155+ ['501 Not Implemented\n\nThe server has either erred or is '\
 156+ 'incapable of performing the requested operation.\n\n Unknown '\
 157+ 'Status: 999 '])
 158+
 159+ def test_11(self):
 160+ """#11 Trap URLs that don't match the regexp"""
 161+ app = rewrite.WMFRewrite(FakeApp("404 TryRegexp", {}),self.a)
 162+ req = webob.Request.blank("http://localhost/a",
 163+ environ={'REQUEST_METHOD': 'GET'})
 164+ resp = app(req.environ, start_response)
 165+ self.assertEquals(resp,
 166+ ['400 Bad Request\n\nThe server could not comply with the '\
 167+ 'request since it is either malformed or otherwise '\
 168+ 'incorrect.\n\n Regexp failed: "/a" '])
 169+
 170+if __name__ == '__main__':
 171+ unittest.main()
 172+
Property changes on: trunk/extensions/SwiftMedia/wmf/tests/test_rewrite.py
___________________________________________________________________
Added: svn:eol-style
1173 + native
Added: svn:executable
2174 + *

Status & tagging log