├── requirements.txt ├── packages ├── maxconnect │ ├── __init__.py │ ├── pycharm.py │ └── tomax.py ├── maxrenderelements │ ├── __init__.py │ ├── test │ │ └── test_renderelementmanager.py │ └── RenderElementManager.py ├── maxfumefx │ ├── __init__.py │ └── jobsim.py ├── maxviewport │ ├── test │ │ └── test_maxviewport.py │ └── __init__.py ├── maxfb │ ├── __init__.py │ └── fbauth.py ├── maxhelpers │ ├── MaxPlusConsole.py │ ├── BoundingBox.py │ └── __init__.py └── maxui │ └── __init__.py ├── Externals ├── P4Python_VC11_x64.zip ├── pyzmq.14.3.0-py2.7.zip ├── zeromq-4.0.4_VC11_x64.zip ├── FB_oAuth │ ├── close.html │ ├── getaccesstoken.php │ ├── index.php │ ├── facebook.php │ └── base_facebook.php └── Npp │ ├── shortcuts.xml │ └── userDefineLang.xml ├── Examples ├── fumefx.py ├── grabviewport.py ├── maxplusconsole.py ├── selectbyvolume.py ├── html │ └── index.html ├── facebook.py ├── getnodenames.py ├── addmodifier.py ├── zeromq_server.py ├── zeromq_client.py ├── p4example.py ├── xmlobjects.py ├── pyside_maxui.py ├── pyside_simple.py ├── pyside_3dsmax_parent.py ├── add_pcache.py ├── pyside_example.py ├── simplewebserver.py ├── ui │ └── editor.ui └── pyqt_pyeditor.py ├── main.py ├── .travis.yml ├── run.py ├── README.md └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | PySideKick 2 | facepy -------------------------------------------------------------------------------- /packages/maxconnect/__init__.py: -------------------------------------------------------------------------------- 1 | from maxconnect import pycharm -------------------------------------------------------------------------------- /Externals/P4Python_VC11_x64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycdivfx/YCDIVFX_MaxPlus/HEAD/Externals/P4Python_VC11_x64.zip -------------------------------------------------------------------------------- /Externals/pyzmq.14.3.0-py2.7.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycdivfx/YCDIVFX_MaxPlus/HEAD/Externals/pyzmq.14.3.0-py2.7.zip -------------------------------------------------------------------------------- /Examples/fumefx.py: -------------------------------------------------------------------------------- 1 | import maxfumefx.jobsim as js 2 | 3 | 4 | ffxsim = js.FumeFxJobSim() 5 | 6 | ffxsim.simtodeadline() 7 | -------------------------------------------------------------------------------- /Externals/zeromq-4.0.4_VC11_x64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ycdivfx/YCDIVFX_MaxPlus/HEAD/Externals/zeromq-4.0.4_VC11_x64.zip -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Demonstrates outputting text the MAXScript listener. 3 | ''' 4 | 5 | import MaxPlus 6 | MaxPlus.Core.WriteLine("hello world") -------------------------------------------------------------------------------- /Examples/grabviewport.py: -------------------------------------------------------------------------------- 1 | import maxviewport 2 | 3 | 4 | if __name__ == '__main__': 5 | vpGrab = maxviewport.VpGrab() 6 | bmp = vpGrab.ActiveViewport() 7 | bmp.Display() 8 | -------------------------------------------------------------------------------- /Examples/maxplusconsole.py: -------------------------------------------------------------------------------- 1 | from PySideKick import Console 2 | from maxhelpers import MaxPlusConsole 3 | 4 | main = MaxPlusConsole.MaxWidget() 5 | main.setCentralWidget(Console.QPythonConsole()) 6 | main.show() -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies 5 | install: "pip install -r requirements.txt --use-mirrors" 6 | # command to run tests 7 | script: nosetests -w packages 8 | notifications: 9 | email: false -------------------------------------------------------------------------------- /Examples/selectbyvolume.py: -------------------------------------------------------------------------------- 1 | import MaxPlus 2 | from maxhelpers import BoundingBox 3 | 4 | # Grab selected object to use as volume 5 | obj = MaxPlus.SelectionManager.GetNode(0) 6 | 7 | if obj: 8 | BoundingBox.SelectByVolume(obj) 9 | else: 10 | print 'Select an object!' -------------------------------------------------------------------------------- /Externals/FB_oAuth/close.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Close 4 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | Execute in 3ds Max: 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /Examples/facebook.py: -------------------------------------------------------------------------------- 1 | import maxfb 2 | 3 | 4 | if __name__ == '__main__': 5 | fbc = maxfb.FbConnector() 6 | 7 | print 'Your Facebook username is:' + fbc.getUsername() 8 | 9 | #message = '3dsmax python connector with automatic authentication' 10 | #fbc.postMessage(message) 11 | #imagefilename = r'c:\test.jpg' 12 | #fbc.postImage('', imagefilename) -------------------------------------------------------------------------------- /Examples/getnodenames.py: -------------------------------------------------------------------------------- 1 | import MaxPlus 2 | 3 | 4 | def getselectednodenames(): 5 | for node in MaxPlus.SelectionManager.Nodes: 6 | print node.Name 7 | 8 | def getscenenodenames(): 9 | for node in MaxPlus.Core.GetRootNode().Children: 10 | print node.Name 11 | 12 | if __name__ == '__main__': 13 | getselectednodenames() 14 | getscenenodenames() -------------------------------------------------------------------------------- /packages/maxrenderelements/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | ''' 3 | Created on 07/11/2013 4 | 5 | @author: Daniel Santana 6 | ''' 7 | from RenderElementManager import RenderElement, RenderElementManager 8 | 9 | __author__ = 'dgsantana' 10 | __version__ = '1.0' 11 | __copyright__ = 'Copyright 2013, Daniel Santana' 12 | __date__ = '11-11-2013' 13 | 14 | RenderElement 15 | RenderElementManager 16 | -------------------------------------------------------------------------------- /Examples/addmodifier.py: -------------------------------------------------------------------------------- 1 | import MaxPlus 2 | 3 | 4 | def addmodifier(nodes): 5 | for node in nodes: 6 | mod = MaxPlus.Factory.CreateObjectModifier(MaxPlus.ClassIds.Noisemodifier) 7 | for param in mod.ParameterBlock: 8 | print param.Name 9 | mod.ParameterBlock.seed.Value = 12345 10 | node.AddModifier(mod) 11 | 12 | if __name__ == '__main__': 13 | addmodifier(MaxPlus.SelectionManager.Nodes) -------------------------------------------------------------------------------- /packages/maxfumefx/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import MaxPlus 3 | except ImportError: 4 | MaxPlus = None 5 | print 'MaxPlus not present' 6 | 7 | 8 | def getfumefxgrids(): 9 | """Helper function that returns a list with all FumeFx Grids in the scene 10 | """ 11 | nodes = [] 12 | for node in MaxPlus.Core.GetRootNode().Children: 13 | if 'fumefx' in node.Object.GetClassName().lower(): 14 | nodes.append(node) 15 | 16 | return nodes -------------------------------------------------------------------------------- /Examples/zeromq_server.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | 3 | 4 | def server(port='5556', req=5): 5 | context = zmq.Context() 6 | socket = context.socket(zmq.REP) 7 | socket.bind('tcp://*:%s' % port) 8 | print 'Running server on port: ', port 9 | # serves only 5 request and dies 10 | for reqnum in range(req): 11 | # Wait for next request from client 12 | message = socket.recv() 13 | print 'Received request #%s: %s' % (reqnum, message) 14 | socket.send('World from %s' % port) 15 | 16 | if __name__ == "__main__": 17 | server() -------------------------------------------------------------------------------- /Examples/zeromq_client.py: -------------------------------------------------------------------------------- 1 | import time 2 | import zmq 3 | 4 | 5 | def client(port="5556", req=5): 6 | context = zmq.Context() 7 | print "Connecting to server with ports %s" % port 8 | socket = context.socket(zmq.REQ) 9 | socket.connect ("tcp://localhost:%s" % port) 10 | for request in range (req): 11 | print "Sending request ", request,"..." 12 | socket.send ("Hello") 13 | message = socket.recv() 14 | print "Received reply ", request, "[", message, "]" 15 | time.sleep (1) 16 | 17 | if __name__ == "__main__": 18 | client() -------------------------------------------------------------------------------- /packages/maxconnect/pycharm.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Based from Sublime3dsmax : http://cbuelter.de/?p=535 3 | Removed unecessary Sublime portions of the script 4 | ''' 5 | import os 6 | 7 | import tomax 8 | 9 | MAX_NOT_FOUND = r"Could not find a 3ds Max instance." 10 | RECORDER_NOT_FOUND = r"Could not find MAXScript Macro Recorder" 11 | 12 | def run(cmd): 13 | if not tomax.connectToMax(): # Always connect first 14 | print (MAX_NOT_FOUND) 15 | return 16 | if tomax.gMiniMacroRecorder: 17 | tomax.fireCommand(cmd) 18 | tomax.gMiniMacroRecorder = None # Reset for next reconnect 19 | else: 20 | print(RECORDER_NOT_FOUND) -------------------------------------------------------------------------------- /Externals/FB_oAuth/getaccesstoken.php: -------------------------------------------------------------------------------- 1 | 'yourAppID', 7 | 'secret' => 'yourAppSecret', 8 | 'fileUpload' => false, // optional 9 | 'allowSignedRequest' => false, // optional, but should be set to false for non-canvas apps 10 | )); 11 | 12 | $user = $facebook->getUser(); 13 | 14 | if ($user) { 15 | $access_token = $facebook->getAccessToken(); 16 | // Pass over the access token back to our python webserver to be caught by the BaseHTTPRequestHandler and stored 17 | header("Location: http://127.0.0.1?access_token=". $access_token); 18 | } 19 | ?> -------------------------------------------------------------------------------- /Examples/p4example.py: -------------------------------------------------------------------------------- 1 | from P4 import P4, P4Exception # Import the module 2 | 3 | p4 = P4() # Create the P4 instance 4 | p4.port = 'localhost:1666' 5 | p4.user = 'fred' 6 | p4.client = 'fred-ws' # Set some environment variables 7 | 8 | try: # Catch exceptions with try/except 9 | p4.connect() # Connect to the Perforce Server 10 | info = p4.run('info') # Run "p4 info" (returns a dict) 11 | for key in info[1]: # and display all key-value pairs 12 | print '%s = %s' % (key, info[1][key]) 13 | p4.disconnect() # Disconnect from the Server 14 | except P4Exception: 15 | for e in p4.errors: # Display errors 16 | print e -------------------------------------------------------------------------------- /packages/maxviewport/test/test_maxviewport.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_equal 2 | 3 | import maxviewport 4 | 5 | 6 | class test_vpgrab(): 7 | @classmethod 8 | def setUpClass (self): 9 | self.vp = maxviewport.VpGrab() 10 | 11 | def testMethod_Gw(self): 12 | assert_equal(self.vp.method, 'gw') 13 | 14 | def testMethod_Viewport(self): 15 | self.vp.method = maxviewport.VpGrabType.viewport 16 | assert_equal(self.vp.method, 'viewport') 17 | 18 | class test_vpgrabtype(): 19 | @classmethod 20 | def setUpClass(self): 21 | self.vptype = maxviewport.VpGrabType 22 | 23 | def testGw(self): 24 | assert_equal(self.vptype.gw, 'gw') 25 | 26 | def testViewport(self): 27 | assert_equal(self.vptype.viewport, 'viewport') -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from optparse import OptionParser 4 | 5 | packagesdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'packages') 6 | sys.path.append(packagesdir) 7 | 8 | import maxconnect 9 | 10 | parser = OptionParser() 11 | parser.add_option('-f', dest='filename', help='Maxscript FILENAME') 12 | 13 | (options, args) = parser.parse_args() 14 | 15 | if __name__ == '__main__': 16 | if options.filename: 17 | filename, extension = os.path.splitext(options.filename) 18 | if extension == '.py': 19 | cmd = r'python.ExecuteFile @"%s";' % options.filename 20 | elif extension == '.ms' or extension == '.mcr': 21 | cmd = r'fileIn @"%s";' % options.filename 22 | else: 23 | cmd = r'print "Invalid filetype";' 24 | maxconnect.pycharm.run(cmd) -------------------------------------------------------------------------------- /Externals/FB_oAuth/index.php: -------------------------------------------------------------------------------- 1 | 'yourAppID', 7 | 'secret' => 'yourAppSecret', 8 | 'fileUpload' => false, // optional 9 | 'allowSignedRequest' => false, // optional, but should be set to false for non-canvas apps 10 | )); 11 | 12 | $post_login = 'http://www.yourserver.com/getaccesstoken.php'; 13 | 14 | // Get User ID 15 | $uid = $facebook->getUser(); 16 | 17 | // If the user is logged in and with permissions, let's move on and get the access token. 18 | if(!empty($udid)){ 19 | header("Location: ". $post_login); 20 | } 21 | else 22 | { 23 | $params = array( 24 | redirect_uri => $post_login, 25 | scope => 'read_stream,publish_stream' 26 | ); 27 | $loginUrl = $facebook->getLoginUrl($params); 28 | 29 | header("Location: ". $loginUrl); 30 | } 31 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | YCDIVFX MaxPlus Packages 2 | ======================== 3 | [![Build Status](https://travis-ci.org/arturleao/YCDIVFX_MaxPlus.png?branch=master)](https://travis-ci.org/arturleao/YCDIVFX_MaxPlus) 4 | 5 | * Add the 'packages' folder to your PYTHONPATH envirnoment variable 6 | * maxpycharm based on Sublime3dsMax (http://cbuelter.de/?p=535) 7 | * maxfb has a depedency on facepy (https://github.com/jgorset/facepy) 8 | * maxplusconsole as a dependency on PySideKick (https://github.com/cloudmatrix/pysidekick/) 9 | 10 | PyCharm usage 11 | ------------- 12 | 13 | Create a new configuration. Fill in with the following values: 14 | 15 | Script: C:\YCDIVFX\MaxPlus\Examples\run.py 16 | Script parameters: -f C:\YCDIVFX\MaxPlus\Examples\pyqt_pyeditor.py 17 | 18 | Command-line usage 19 | ------------- 20 | 21 | python.exe C:\YCDIVFX\MaxPlus\MyExamples\run.py -f C:\YCDIVFX\MaxPlus\Examples\pyqt_pyeditor.py 22 | 23 | (3ds Max needs to be open. You need to use full paths) -------------------------------------------------------------------------------- /packages/maxfb/__init__.py: -------------------------------------------------------------------------------- 1 | from facepy import GraphAPI 2 | import fbauth 3 | 4 | 5 | class FbConnector(): 6 | def __init__(self): 7 | self.token = fbauth.getAccessToken() 8 | self.graph = GraphAPI(self.token) 9 | 10 | def postImage(self, caption, filename): 11 | """Posts an image file into FB 12 | :param str caption: a valid string 13 | :param str filename: a valid path to an image file 14 | """ 15 | try: 16 | fimage = open(filename, 'rb') 17 | self.graph.post(path="me/photos", caption=caption, source=fimage) 18 | except: 19 | print 'Error submitting image' 20 | 21 | def postMessage(self, text): 22 | """Posts a message into FB 23 | :param str text: a valid string 24 | """ 25 | try: 26 | self.graph.post(path='me/feed', message=text) 27 | except: 28 | print 'There was an error posting the message' 29 | 30 | def getUsername(self): 31 | """Gets the current logged in username 32 | :rtype: str 33 | """ 34 | try: 35 | r = self.graph.get('me?fields=name') 36 | return r['name'] 37 | except: 38 | print 'Error retrieving name' -------------------------------------------------------------------------------- /packages/maxfb/fbauth.py: -------------------------------------------------------------------------------- 1 | import urlparse 2 | import BaseHTTPServer 3 | import webbrowser 4 | 5 | ACCESS_TOKEN = None 6 | auth_url = 'http://www.youcandoitvfx.com/fb/' 7 | server_host = '127.0.0.1' 8 | server_port = 80 9 | 10 | 11 | class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): 12 | def do_GET(self): 13 | global ACCESS_TOKEN 14 | 15 | self.send_response(301) 16 | self.send_header('Location', auth_url + 'close.html') 17 | self.end_headers() 18 | 19 | parsed_path = urlparse.urlparse(self.path) 20 | try: 21 | params = dict([p.split('=') for p in parsed_path[4].split('&')]) 22 | except: 23 | params = {} 24 | 25 | _access_token = params.get('access_token', 'error') 26 | if (_access_token != 'error') and (len(_access_token) != 0): 27 | ACCESS_TOKEN = _access_token 28 | 29 | def getAccessToken(): 30 | global ACCESS_TOKEN 31 | 32 | ACCESS_TOKEN = None 33 | 34 | server_class = BaseHTTPServer.HTTPServer 35 | httpd = server_class((server_host, server_port), MyHandler) 36 | webbrowser.open(auth_url) 37 | while ACCESS_TOKEN is None: 38 | httpd.handle_request() 39 | 40 | httpd.server_close() 41 | 42 | return ACCESS_TOKEN -------------------------------------------------------------------------------- /packages/maxrenderelements/test/test_renderelementmanager.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 11/11/2013 3 | 4 | @author: dgsantana 5 | ''' 6 | import unittest 7 | import maxrenderelements 8 | 9 | 10 | class Test(unittest.TestCase): 11 | 12 | def setUp(self): 13 | print 'Setup' 14 | self._rm = maxrenderelements.RenderElementManager() 15 | self._rm.RemoveAllElements() 16 | self._rm.Active = True 17 | self._rm.Display = True 18 | 19 | def tearDown(self): 20 | print 'Cleanup' 21 | self._rm.RemoveAllElements() 22 | self._rm = None 23 | 24 | def testElementCount(self): 25 | self._rm.RemoveAllElements() 26 | self.failUnless(len(self._rm) == 0) 27 | 28 | def testAddElement(self): 29 | self._rm.RemoveAllElements() 30 | self._rm.AddElement('alphaRenderElement') 31 | self.failIf(len(self._rm) == 0) 32 | 33 | def testElementMXSProperty(self): 34 | self._rm.RemoveAllElements() 35 | element = self._rm.AddElement('ZRenderElement') 36 | self.failUnless(element.zMin == 100) 37 | 38 | def testActive(self): 39 | self._rm.Active = False 40 | self.failUnless(self._rm.Active == False) 41 | 42 | def testDisplay(self): 43 | self._rm.Display = False 44 | self.failUnless(self._rm.Display == False) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /packages/maxfumefx/jobsim.py: -------------------------------------------------------------------------------- 1 | try: 2 | import MaxPlus 3 | except ImportError: 4 | MaxPlus = None 5 | print 'MaxPlus not present' 6 | 7 | import maxfumefx 8 | reload(maxfumefx) 9 | 10 | 11 | class FumeFxJobSim: 12 | def __init__(self): 13 | """Class to represent a FumeFx Job in simulation mode""" 14 | pass 15 | 16 | def sendjobffx(self, node): 17 | rs = MaxPlus.RenderSettings 18 | 19 | rs.CloseDialog() 20 | 21 | timetype= rs.GetTimeType() 22 | savefile = rs.GetSaveFile() 23 | height = rs.GetHeight() 24 | width = rs.GetWidth() 25 | 26 | rs.SetTimeType(1) 27 | rs.SetSaveFile(False) 28 | rs.SetHeight(25) 29 | rs.SetWidth(25) 30 | 31 | MaxPlus.Core.EvalMAXScript('$' + node.Name + '.BackBurnerSim = True') 32 | MaxPlus.Core.EvalMAXScript('SMTDSettings.JobName = GetFilenameFile(maxfilename) + \"_FFXSim_' + node.Name + '\"') 33 | MaxPlus.Core.EvalMAXScript('SMTDFunctions.SubmitJob()') 34 | MaxPlus.Core.EvalMAXScript('$' + node.Name + '.BackBurnerSim = False') 35 | 36 | rs.SetTimeType(timetype) 37 | rs.SetSaveFile(savefile) 38 | rs.SetHeight(height) 39 | rs.SetWidth(width) 40 | 41 | def simtodeadline(self): 42 | fumefx_containers = maxfumefx.getfumefxgrids() 43 | for fumefx in fumefx_containers: 44 | self.sendjobffx(fumefx) -------------------------------------------------------------------------------- /Examples/xmlobjects.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import MaxPlus 4 | 5 | 6 | def prettyxml(elem, level=0): 7 | i = "\n" + level * " " 8 | if len(elem): 9 | if not elem.text or not elem.text.strip(): 10 | elem.text = i + " " 11 | if not elem.tail or not elem.tail.strip(): 12 | elem.tail = i 13 | for elem in elem: 14 | prettyxml(elem, level + 1) 15 | if not elem.tail or not elem.tail.strip(): 16 | elem.tail = i 17 | else: 18 | if level and (not elem.tail or not elem.tail.strip()): 19 | elem.tail = i 20 | 21 | 22 | def main(): 23 | MaxPlus.Core.EvalMAXScript('clearListener()') 24 | 25 | if MaxPlus.Core.GetRootNode().GetNumChildren() == 0: 26 | print 'No objects in scene' 27 | return 28 | 29 | xmlroot = ET.Element('RootNode') 30 | document = ET.ElementTree(xmlroot) 31 | 32 | for obj in MaxPlus.Core.GetRootNode().Children: 33 | assert isinstance(obj, MaxPlus.INode) 34 | node = ET.SubElement(xmlroot,'Object') 35 | assert isinstance(node, ET.Element) 36 | node.set('Name', obj.Name) 37 | node.set('WireColor', str(obj.WireColor)) 38 | 39 | prettyxml(xmlroot) 40 | 41 | print ET.tostring(xmlroot) 42 | document.write(r'C:\3dsmax.xml', encoding='utf-8', xml_declaration=True,) 43 | 44 | if __name__ == '__main__': 45 | main() -------------------------------------------------------------------------------- /Examples/pyside_maxui.py: -------------------------------------------------------------------------------- 1 | from PySide import QtGui, QtCore 2 | 3 | import MaxPlus 4 | 5 | from maxui import MaxWindow 6 | 7 | 8 | def make_cylinder(): 9 | obj = MaxPlus.Factory.CreateGeomObject(MaxPlus.ClassIds.Cylinder) 10 | obj.ParameterBlock.Radius.Value = 10.0 11 | obj.ParameterBlock.Height.Value = 30.0 12 | MaxPlus.Factory.CreateNode(obj) 13 | time = MaxPlus.Core.GetCurrentTime() 14 | MaxPlus.ViewportManager.RedrawViews(time) 15 | 16 | 17 | @MaxWindow 18 | class Widget(QtGui.QWidget): 19 | def __init__(self, parent=None, **kwargs): 20 | super(Widget, self).__init__(parent) 21 | 22 | self.btnRun = QtGui.QPushButton('Run') 23 | self.btnClose = QtGui.QPushButton('Close') 24 | 25 | layout = QtGui.QVBoxLayout(self) 26 | layout.addWidget(self.btnRun) 27 | layout.addWidget(self.btnClose) 28 | self.setLayout(layout) 29 | self.setWindowTitle('Simple 3ds Max PySide Example') 30 | 31 | self.setWindowFlags(QtCore.Qt.Tool | 32 | QtCore.Qt.WindowStaysOnTopHint | 33 | QtCore.Qt.MSWindowsFixedSizeDialogHint) 34 | 35 | self.btnRun.clicked.connect(make_cylinder) 36 | self.btnClose.clicked.connect(self.close) 37 | self.setGeometry(100, 100, 250, 80) 38 | print self.frameGeometry() 39 | 40 | 41 | app = QtGui.QApplication.instance() 42 | if not app: 43 | app = QtGui.QApplication([]) 44 | 45 | 46 | def main(): 47 | widget = Widget(parented=True) 48 | widget.show() 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /Examples/pyside_simple.py: -------------------------------------------------------------------------------- 1 | from PySide import QtGui, QtCore, shiboken 2 | 3 | import MaxPlus 4 | 5 | 6 | def make_cylinder(): 7 | obj = MaxPlus.Factory.CreateGeomObject(MaxPlus.ClassIds.Cylinder) 8 | obj.ParameterBlock.Radius.Value = 10.0 9 | obj.ParameterBlock.Height.Value = 30.0 10 | MaxPlus.Factory.CreateNode(obj) 11 | time = MaxPlus.Core.GetCurrentTime() 12 | MaxPlus.ViewportManager.RedrawViews(time) 13 | 14 | return 15 | 16 | 17 | class _GCProtector(object): 18 | controls = [] 19 | 20 | 21 | def main(): 22 | app = QtGui.QApplication.instance() 23 | if not app: 24 | app = QtGui.QApplication([]) 25 | 26 | mainWindow = QtGui.QMainWindow() 27 | _GCProtector.controls.append(mainWindow) 28 | mainWindow.setWindowTitle('Simple tool') 29 | mainWindow.resize(250,50) 30 | 31 | widget = QtGui.QWidget() 32 | 33 | main_layout = QtGui.QVBoxLayout() 34 | label = QtGui.QLabel("Click button to create a cylinder in the scene") 35 | main_layout.addWidget(label) 36 | 37 | cylinder_btn = QtGui.QPushButton("Cylinder") 38 | main_layout.addWidget(cylinder_btn) 39 | widget.setLayout(main_layout) 40 | 41 | mainWindow.setCentralWidget(widget) 42 | 43 | cylinder_btn.clicked.connect(make_cylinder) 44 | 45 | mainWindow.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.MSWindowsFixedSizeDialogHint) 46 | mainWindow.setAttribute(QtCore.Qt.WA_DeleteOnClose) 47 | mainWindow.setAttribute(QtCore.Qt.WA_QuitOnClose) 48 | mainWindow.setAttribute(QtCore.Qt.WA_X11NetWmWindowTypeDialog) 49 | 50 | mainWindow.show() 51 | 52 | if __name__ == '__main__': 53 | main() -------------------------------------------------------------------------------- /Examples/pyside_3dsmax_parent.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from PySide import QtGui, QtCore 3 | 4 | import MaxPlus 5 | 6 | 7 | def make_cylinder(): 8 | obj = MaxPlus.Factory.CreateGeomObject(MaxPlus.ClassIds.Cylinder) 9 | obj.ParameterBlock.Radius.Value = 10.0 10 | obj.ParameterBlock.Height.Value = 30.0 11 | MaxPlus.Factory.CreateNode(obj) 12 | time = MaxPlus.Core.GetCurrentTime() 13 | MaxPlus.ViewportManager.RedrawViews(time) 14 | 15 | 16 | class _GCProtector(object): 17 | controls = [] 18 | 19 | 20 | def main(): 21 | app = QtGui.QApplication.instance() 22 | if not app: 23 | app = QtGui.QApplication([]) 24 | 25 | widget = QtGui.QWidget() 26 | _GCProtector.controls.append(widget) 27 | widget.setWindowTitle('Simple tool') 28 | widget.resize(250,50) 29 | 30 | main_layout = QtGui.QVBoxLayout() 31 | label = QtGui.QLabel("Click button to create a cylinder in the scene") 32 | main_layout.addWidget(label) 33 | 34 | cylinder_btn = QtGui.QPushButton("Cylinder") 35 | main_layout.addWidget(cylinder_btn) 36 | widget.setLayout(main_layout) 37 | 38 | cylinder_btn.clicked.connect(make_cylinder) 39 | 40 | widget.setWindowFlags(QtCore.Qt.Tool | 41 | QtCore.Qt.WindowStaysOnTopHint | 42 | QtCore.Qt.MSWindowsFixedSizeDialogHint) 43 | widget.setAttribute(QtCore.Qt.WA_DeleteOnClose) 44 | widget.setAttribute(QtCore.Qt.WA_QuitOnClose) 45 | widget.setAttribute(QtCore.Qt.WA_X11NetWmWindowTypeDialog) 46 | 47 | widget.show() 48 | 49 | capsule = widget.effectiveWinId() 50 | ctypes.pythonapi.PyCObject_AsVoidPtr.restype = ctypes.c_void_p 51 | ctypes.pythonapi.PyCObject_AsVoidPtr.argtypes = [ctypes.py_object] 52 | ptr = ctypes.pythonapi.PyCObject_AsVoidPtr(capsule) 53 | 54 | MaxPlus.Win32.Set3dsMaxAsParentWindow(ptr) 55 | 56 | if __name__ == '__main__': 57 | main() -------------------------------------------------------------------------------- /packages/maxhelpers/MaxPlusConsole.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reference: http://area.autodesk.com/blogs/chris/pyqt-ui-in-3ds-max-2014-extension 3 | """ 4 | from PySide import QtGui, QtCore 5 | import MaxPlus 6 | 7 | app = QtGui.QApplication.instance() 8 | if not app: 9 | app = QtGui.QApplication([]) 10 | 11 | 12 | class _Widgets(object): 13 | """ Used to store all widget instances and protect them from the garbage collector """ 14 | _instances = [] 15 | 16 | 17 | class _FocusFilter(QtCore.QObject): 18 | """ Used to filter events to properly manage focus in 3ds Max. This is a hack to deal with the fact 19 | that mixing Qt and Win32 causes focus events to not get triggered as expected. """ 20 | 21 | def eventFilter(self, obj, event): 22 | if event.type() == QtCore.QEvent.MouseButtonPress: 23 | MaxPlus.CUI.DisableAccelerators() 24 | return False 25 | 26 | 27 | class MaxWidget(QtGui.QMainWindow): 28 | """ Note: this does not work automatically when switching focus from the rest of 3ds Max 29 | to the PyQt UI. This is why we have to install an event filter. """ 30 | 31 | #def focusInEvent(self, event): 32 | # MaxPlus.CUI.DisableAccelerators() 33 | # 34 | #def focusOutEvent(self, event): 35 | # MaxPlus.CUI.EnableAccelerators() 36 | 37 | def __init__(self): 38 | QtGui.QMainWindow.__init__(self) 39 | #_Widgets._instances.append(self) 40 | # Install an event filter. Keep the pointer in this object so it isn't collected, and can be removed. 41 | self.filter = _FocusFilter(self) 42 | app.installEventFilter(self.filter) 43 | 44 | def closeEvent(self, event): 45 | if self in _Widgets._instances: 46 | _Widgets._instances.remove(self) 47 | # Very important, otherwise the application event filter will stick around for 48 | app.removeEventFilter(self.filter) 49 | 50 | 51 | -------------------------------------------------------------------------------- /Examples/add_pcache.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PySide import QtGui 3 | 4 | import MaxPlus 5 | 6 | 7 | class MyWindow(QtGui.QDialog): 8 | def __init__(self, parent=None): 9 | super(MyWindow, self).__init__(parent) 10 | self.lbl_project = QtGui.QLabel('Project Path...') 11 | self.btn_project = QtGui.QPushButton('Select project folder') 12 | self.txt_prefix = QtGui.QLineEdit() 13 | self.btn_run = QtGui.QPushButton('Run') 14 | 15 | layout = QtGui.QVBoxLayout() 16 | layout.addWidget(self.lbl_project) 17 | layout.addWidget(self.btn_project) 18 | layout.addWidget(self.txt_prefix) 19 | layout.addWidget(self.btn_run) 20 | self.setLayout(layout) 21 | 22 | self.txt_prefix.setText('whatever_') 23 | 24 | self.btn_project.clicked.connect(self.select_project_folder) 25 | self.btn_run.clicked.connect(self.addCache) 26 | 27 | self.setupPaths() 28 | self.updateUI() 29 | 30 | def setupPaths(self): 31 | self.projectpath = os.path.abspath('c:\\') 32 | self.object_prefix = self.txt_prefix.text() 33 | 34 | def updateUI(self): 35 | self.lbl_project.setText(self.projectpath) 36 | 37 | def select_project_folder(self): 38 | directory = QtGui.QFileDialog.getExistingDirectory(self, 'Select project folder', self.projectpath) 39 | if directory: 40 | self.projectpath = os.path.abspath(directory) 41 | self.updateUI() 42 | 43 | def addCache(self): 44 | for node in MaxPlus.SelectionManager_GetNodes(): 45 | cachename = os.path.join(self.projectpath, 'cache', str(self.object_prefix) + node.Name + '.pc2') 46 | if os.path.exists(cachename): 47 | mod = MaxPlus.Factory.CreateWorldSpaceModifier(MaxPlus.ClassIds.Point_CacheSpacewarpModifier) 48 | mod.ParameterBlock.Filename.Value = os.path.join(self.projectpath, 'cache', cachename) 49 | node.AddModifier(mod) 50 | 51 | 52 | if __name__ == '__main__': 53 | app = QtGui.QApplication.instance() 54 | if not app: 55 | app = QtGui.QApplication([]) 56 | window = MyWindow() 57 | window.show() 58 | app.exec_() -------------------------------------------------------------------------------- /packages/maxhelpers/BoundingBox.py: -------------------------------------------------------------------------------- 1 | """ 2 | Based on the work of Eric Spevacek for Maya 3 | http://technicallyitsart.wordpress.com/2013/12/28/maya-python-select-by-volume/ 4 | """ 5 | import MaxPlus 6 | import maxhelpers as mh 7 | 8 | 9 | class BoundingBox(): 10 | """ Helper class for bounding box-related calculations.""" 11 | @classmethod 12 | def FromShape(cls, shapeObj): 13 | """ Constructor method to create a bounding box from a shape. 14 | :param MaxPlus.INode shapeObj: Node to extract bounding box 15 | """ 16 | boundingBox = BoundingBox() 17 | bb = mh.GetWorldBoundBox(shapeObj) 18 | boundingBox.minX = bb.Min.X 19 | boundingBox.minY = bb.Min.Y 20 | boundingBox.minZ = bb.Min.Z 21 | boundingBox.maxX = bb.Max.X 22 | boundingBox.maxY = bb.Max.Y 23 | boundingBox.maxZ = bb.Max.Z 24 | return boundingBox 25 | 26 | def ContainsShape(self, shape): 27 | """ Returns whether or not a shape is intersecting with this bounding box. 28 | """ 29 | shapeBB = BoundingBox.FromShape(shape) 30 | return (shapeBB.minX < self.maxX and shapeBB.maxX > self.minX) and \ 31 | (shapeBB.minY < self.maxY and shapeBB.maxY > self.minY) and\ 32 | (shapeBB.maxZ < self.maxZ and shapeBB.maxZ > self.minZ) 33 | 34 | 35 | def SelectByVolume(volumeObj): 36 | """ Selects all transforms in the scene that are within the specified argument's bounding box volume. """ 37 | 38 | # Create bounding box class from object 39 | boundingBox = BoundingBox.FromShape(volumeObj) 40 | 41 | # Get all scene objects asides from bounding box 42 | sceneObjs = [obj for obj in MaxPlus.Core.GetRootNode().Children 43 | if obj != volumeObj] 44 | 45 | # Compare against every object in our scene to determine what is in our volume 46 | newSelection = MaxPlus.INodeTab() 47 | for obj in sceneObjs: 48 | # If the shape's bounding box intersects with the volume's bounding box 49 | if boundingBox.ContainsShape(obj): 50 | newSelection.Append(obj) 51 | 52 | # Update selection to nodes contained in volume 53 | if newSelection: 54 | MaxPlus.SelectionManager.ClearNodeSelection() 55 | MaxPlus.SelectionManager.SelectNodes(newSelection) -------------------------------------------------------------------------------- /Externals/Npp/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | firefox "$(FULL_CURRENT_PATH)" 11 | iexplore "$(FULL_CURRENT_PATH)" 12 | chrome "$(FULL_CURRENT_PATH)" 13 | safari "$(FULL_CURRENT_PATH)" 14 | http://www.php.net/%20$(CURRENT_WORD) 15 | http://www.google.com/search?q=$(CURRENT_WORD) 16 | http://en.wikipedia.org/wiki/Special:Search?search=$(CURRENT_WORD) 17 | $(NPP_DIRECTORY)\notepad++.exe $(CURRENT_WORD) 18 | $(NPP_DIRECTORY)\notepad++.exe $(CURRENT_WORD) -nosession -multiInst 19 | explorer $(CURRENT_DIRECTORY) 20 | cmd /K cd /d $(CURRENT_DIRECTORY) 21 | outlook /a "$(FULL_CURRENT_PATH)" 22 | "%ADSK_3DSMAX_x64_2015%python\python.exe" "C:\YCDIVFX_MaxPlus\run.py" -f "$(FULL_CURRENT_PATH)" 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Examples/pyside_example.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | from PySide import QtGui, QtCore 4 | 5 | import MaxPlus 6 | 7 | 8 | def make_cylinder(): 9 | obj = MaxPlus.Factory.CreateGeomObject(MaxPlus.ClassIds.Cylinder) 10 | obj.ParameterBlock.Radius.Value = 10.0 11 | obj.ParameterBlock.Height.Value = 30.0 12 | MaxPlus.Factory.CreateNode(obj) 13 | time = MaxPlus.Core.GetCurrentTime() 14 | MaxPlus.ViewportManager.RedrawViews(time) 15 | 16 | 17 | class Widget(QtGui.QWidget): 18 | def __init__(self, parent=None, stylename='windows', theme='dark'): 19 | super(Widget, self).__init__(parent) 20 | 21 | self.btnRun = QtGui.QPushButton('Run') 22 | self.btnClose = QtGui.QPushButton('Close') 23 | 24 | layout = QtGui.QVBoxLayout(self) 25 | layout.addWidget(self.btnRun) 26 | layout.addWidget(self.btnClose) 27 | self.setLayout(layout) 28 | self.setWindowTitle('Simple 3ds Max PySide Example') 29 | 30 | self.setWindowFlags(QtCore.Qt.Tool | 31 | QtCore.Qt.WindowStaysOnTopHint | 32 | QtCore.Qt.MSWindowsFixedSizeDialogHint) 33 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 34 | 35 | self.btnRun.clicked.connect(make_cylinder) 36 | self.btnClose.clicked.connect(self.close) 37 | self.setGeometry(100, 100, 250, 80) 38 | print self.frameGeometry() 39 | 40 | self.setupUI(stylename, theme) 41 | 42 | def setupUI(self, stylename, theme): 43 | for control in self.children(): 44 | if hasattr(control, 'setStyle'): 45 | control.setStyle(QtGui.QStyleFactory.create(stylename)) 46 | 47 | if theme.lower() == 'dark': 48 | self.setStyleSheet('QWidget {' 49 | 'background: #444444;' 50 | 'color: #d8d8d8;' 51 | 'selection-color: black;' 52 | 'selection-background-color: #3399ff;' 53 | '}') 54 | else: 55 | self.setStyleSheet('QWidget {' 56 | 'background: #E6E6E6;' 57 | 'color: #000000;' 58 | 'selection-color: black;' 59 | 'selection-background-color: #3399ff;' 60 | '}') 61 | 62 | 63 | class _GCProtector(object): 64 | widgets = [] 65 | 66 | 67 | app = QtGui.QApplication.instance() 68 | if not app: 69 | app = QtGui.QApplication([]) 70 | 71 | 72 | def main(): 73 | widget = Widget(stylename='Plastique', theme='light') 74 | _GCProtector.widgets.append(widget) 75 | widget.show() 76 | 77 | capsule = widget.effectiveWinId() 78 | ctypes.pythonapi.PyCObject_AsVoidPtr.restype = ctypes.c_void_p 79 | ctypes.pythonapi.PyCObject_AsVoidPtr.argtypes = [ctypes.py_object] 80 | ptr = ctypes.pythonapi.PyCObject_AsVoidPtr(capsule) 81 | 82 | MaxPlus.Win32.Set3dsMaxAsParentWindow(ptr) 83 | 84 | if __name__ == '__main__': 85 | main() -------------------------------------------------------------------------------- /packages/maxui/__init__.py: -------------------------------------------------------------------------------- 1 | import MaxPlus 2 | from PySide import QtGui, QtCore 3 | 4 | 5 | class _GCProtector(object): 6 | """ 7 | Garbage protector for QWidget classes and subclasses. 8 | """ 9 | widgets = [] 10 | 11 | 12 | def MaxWindow(widget): 13 | """ 14 | Decorator for QWidget, making it work with 3dsmax. 15 | @param QWidget widget - Widget to be handled 16 | @return - The decorated class 17 | """ 18 | orig_init = widget.__init__ 19 | orig_show = widget.show 20 | 21 | def __init__(self, *args, **kwargs): 22 | defs = {'stylename': 'Plastique', 'theme': None, 'parented': False} 23 | orig_init(self, *args, **kwargs) 24 | defs.update(kwargs) 25 | # Make widget garbage collected. 26 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 27 | self._parented = defs['parented'] 28 | self._has_parent = False 29 | self._setupUI(defs['stylename'], defs['theme']) 30 | 31 | def _setupUI(self, stylename, theme): 32 | for control in self.children(): 33 | if hasattr(control, 'setStyle'): 34 | control.setStyle(QtGui.QStyleFactory.create(stylename)) 35 | 36 | # Dark theme detection. 37 | if theme is None and MaxPlus.Core.EvalMAXScript('((colorman.getcolor #window) * 255)[1] < 120' 38 | ' and ((colorman.getcolor #window) * 255)[2] < 120' 39 | ' and ((colorman.getcolor #window) * 255)[3] < 120'): 40 | theme = 'dark' 41 | else: 42 | theme = 'light' 43 | 44 | if theme.lower() == 'dark': 45 | self.setStyleSheet('QWidget {' 46 | 'background: #444444;' 47 | 'color: #d8d8d8;' 48 | 'selection-color: black;' 49 | 'selection-background-color: #3399ff;' 50 | '}') 51 | else: 52 | self.setStyleSheet('QWidget {' 53 | 'background: #E6E6E6;' 54 | 'color: #000000;' 55 | 'selection-color: black;' 56 | 'selection-background-color: #3399ff;' 57 | '}') 58 | 59 | def show(self): 60 | if self not in _GCProtector.widgets: 61 | _GCProtector.widgets.append(self) 62 | orig_show(self) 63 | if self._parented and not self._has_parent: 64 | import ctypes 65 | capsule = self.effectiveWinId() 66 | ctypes.pythonapi.PyCObject_AsVoidPtr.restype = ctypes.c_void_p 67 | ctypes.pythonapi.PyCObject_AsVoidPtr.argtypes = [ctypes.py_object] 68 | ptr = ctypes.pythonapi.PyCObject_AsVoidPtr(capsule) 69 | MaxPlus.Win32.Set3dsMaxAsParentWindow(ptr) 70 | self._has_parent = True 71 | 72 | widget._setupUI = _setupUI 73 | widget.show = show 74 | widget.__init__ = __init__ 75 | return widget -------------------------------------------------------------------------------- /packages/maxhelpers/__init__.py: -------------------------------------------------------------------------------- 1 | def mxs_eval(args): 2 | """EvalMAXScript(wchar_t const * s, FPValue result) -> bool 3 | EvalMAXScript(wchar_t const * s) -> FPValue 4 | """ 5 | import MaxPlus 6 | 7 | return MaxPlus.Core.EvalMAXScript(args) 8 | 9 | 10 | class BitmapTypes(object): 11 | # Not allocated yet 12 | BMM_NO_TYPE = 0 13 | # 1-bit monochrome image 14 | BMM_LINE_ART = 1 15 | # 8-bit paletted image. Each pixel value is an index into the color table. 16 | BMM_PALETTED = 2 17 | # 8-bit grayscale bitmap. 18 | BMM_GRAY_8 = 3 19 | # 16-bit grayscale bitmap. 20 | BMM_GRAY_16 = 4 21 | # 16-bit true color image. 22 | BMM_TRUE_16 = 5 23 | # 32-bit color: 8 bits each for Red, Green, Blue, and Alpha. 24 | BMM_TRUE_32 = 6 25 | # 64-bit color: 16 bits each for Red, Green, Blue, and Alpha. 26 | BMM_TRUE_64 = 7 27 | # 24-bit color: 8 bits each for Red, Green, and Blue. Cannot be written to. 28 | BMM_TRUE_24 = 8 29 | # 48-bit color: 16 bits each for Red, Green, and Blue. Cannot be written to. 30 | BMM_TRUE_48 = 9 31 | # This is the YUV format - CCIR 601. Cannot be written to. 32 | BMM_YUV_422 = 10 33 | # Windows BMP 16-bit color bitmap. Cannot be written to. 34 | BMM_BMP_4 = 11 35 | # Padded 24-bit (in a 32 bit register). Cannot be written to. 36 | BMM_PAD_24 = 12 37 | # Padded 24-bit (in a 32 bit register). Cannot be written to. 38 | BMM_LOGLUV_32 = 13 39 | BMM_LOGLUV_24 = 14 40 | BMM_LOGLUV_24A = 15 41 | # The 'Real Pixel' format. 42 | BMM_REALPIX_32 = 16 43 | # 32-bit floating-point per component (non-compressed), RGB with or without alpha 44 | BMM_FLOAT_RGBA_32 = 17 45 | # 32-bit floating-point (non-compressed), monochrome/grayscale 46 | BMM_FLOAT_GRAY_32 = 18 47 | BMM_FLOAT_RGB_32 = 19 48 | BMM_FLOAT_A_32 = 20 49 | 50 | 51 | class BitmapOpenMode(object): 52 | BMM_NOT_OPEN = 0 # Not opened yet 53 | BMM_OPEN_R = 1 # Read-only 54 | BMM_OPEN_W = 2 # Write-only. No reads will occur 55 | 56 | 57 | class CopyImageOperations(object): 58 | # Copy image to current map size w/cropping if necessary. 59 | COPY_IMAGE_CROP = 0 60 | # This is a resize from 50x50 to 150x150 using this option. 61 | COPY_IMAGE_RESIZE_LO_QUALITY = 1 62 | # This is a resize from 50x50 to 150x150 using this option. 63 | COPY_IMAGE_RESIZE_HI_QUALITY = 2 64 | # Resize based on Image Input Options (BitmapInfo *) 65 | COPY_IMAGE_USE_CUSTOM = 3 66 | 67 | 68 | class ImageFileType(object): 69 | """ Class that holds different image file types """ 70 | JPG = '.jpg' 71 | TGA = '.tga' 72 | BMP = '.bmp' 73 | 74 | def GetWorldBoundBox(node): 75 | """ Gets world boundingbox of node 76 | :param MaxPlus.INode node: a valid path to an image file, no extension required 77 | :rtype: MaxPlus.Box3 78 | """ 79 | import MaxPlus 80 | 81 | # Viewport Manager 82 | vm = MaxPlus.ViewportManager 83 | # Get active viewport 84 | av = vm.GetActiveViewport() 85 | 86 | return node.GetBaseObject().GetWorldBoundBox(node, av) 87 | 88 | def GetLocalBoundBox(node): 89 | """ Gets local boundingbox of node 90 | :param MaxPlus.INode node: a valid path to an image file, no extension required 91 | :rtype: MaxPlus.Box3 92 | """ 93 | import MaxPlus 94 | 95 | # Viewport Manager 96 | vm = MaxPlus.ViewportManager 97 | # Get active viewport 98 | av = vm.GetActiveViewport() 99 | 100 | return node.GetBaseObject().GetLocalBoundBox(node, av) 101 | -------------------------------------------------------------------------------- /packages/maxviewport/__init__.py: -------------------------------------------------------------------------------- 1 | import MaxPlus 2 | from maxhelpers import mxs_eval, BitmapTypes, ImageFileType, CopyImageOperations 3 | 4 | ''' 5 | To disable button labels in maxscript: ViewportButtonMgr.EnableButtons = False 6 | ''' 7 | 8 | class VpGrabType(): 9 | """ 10 | Enumerates the method used for viewport grabbing. 11 | viewport method allows you to grab the viewport with Depht of field and Motion Blur because it includes the results 12 | of maxops.displayActiveCameraViewWithMultiPassEffect() 13 | """ 14 | gw = "gw" 15 | viewport = "viewport" 16 | 17 | 18 | class VpGrab(): 19 | def __init__(self, method=VpGrabType.gw): 20 | """This class provides you methods to grab the 3dsmax viewport""" 21 | self.method = method 22 | 23 | def ActiveViewport(self, filename=(MaxPlus.PathManager.GetRenderOutputDir() 24 | + r'\default.jpg')): 25 | """Grabs viewport to a file on the hard-drive using default viewport size. 26 | 27 | :param str filename: a valid path to an image file 28 | 29 | :rtype: MaxPlus.Bitmap 30 | """ 31 | # Create storage 32 | storage = MaxPlus.Factory.CreateStorage(BitmapTypes.BMM_TRUE_64) 33 | 34 | # Create BitmapInfo 35 | bmi = storage.GetBitmapInfo() 36 | 37 | # Set filename 38 | bmi.SetName(filename) 39 | 40 | # Create bitmap to hold the dib 41 | bmp = MaxPlus.Factory.CreateBitmap() 42 | 43 | # Viewport Manager 44 | vm = MaxPlus.ViewportManager 45 | # Get active viewport 46 | av = vm.GetActiveViewport() 47 | # Grab the viewport dib into the bitmap 48 | av.GetDIB(bmi, bmp) 49 | 50 | # Open bitmap for writing 51 | bmp.OpenOutput(bmi) 52 | bmp.Write(bmi) 53 | bmp.Close(bmi) 54 | 55 | return bmp 56 | 57 | def ActiveViewportSize(self, filename=(MaxPlus.PathManager.GetRenderOutputDir() 58 | + r'\default.jpg'), size=(640, 480)): 59 | """Grabs viewport to a file on the hard-drive in a specific size. 60 | 61 | :param str filename: a valid path to an image file 62 | :param tuple size: a valid list containing width and height 63 | :rtype: MaxPlus.Bitmap 64 | """ 65 | # Create storage 66 | storage = MaxPlus.Factory.CreateStorage(BitmapTypes.BMM_TRUE_64) 67 | 68 | # Create BitmapInfo 69 | bmi = storage.GetBitmapInfo() 70 | 71 | # Create bitmap to hold the dib 72 | grab = MaxPlus.Factory.CreateBitmap() 73 | 74 | # Viewport Manager 75 | vm = MaxPlus.ViewportManager 76 | # Get active viewport 77 | av = vm.GetActiveViewport() 78 | # Grab the viewport dib into the bitmap 79 | av.GetDIB(bmi, grab) 80 | 81 | # Set filename & Size 82 | bmi.SetName(filename) 83 | bmi.SetWidth(size[0]) 84 | bmi.SetHeight(size[1]) 85 | 86 | # Create new bitmap to hold the resized version 87 | bmp = MaxPlus.Factory.CreateBitmap(bmi) 88 | bmp.CopyImage(grab, CopyImageOperations.COPY_IMAGE_RESIZE_HI_QUALITY, 1) 89 | grab.Close(bmi) 90 | 91 | # Open bitmap for writing 92 | bmp.OpenOutput(bmi) 93 | bmp.Write(bmi) 94 | bmp.Close(bmi) 95 | 96 | return bmp 97 | 98 | def tofile(self, filename): 99 | """Grabs viewport to a file on the hard-drive.""" 100 | args = '''grab = ''' + self.method + '''.GetViewportDIB() 101 | grab.filename = @"''' + filename + '''" 102 | save grab 103 | close grab''' 104 | mxs_eval(args) 105 | 106 | def toclipboard(self): 107 | """Grabs viewport to the clipboard.""" 108 | args = '''grab = ''' + self.method + '''.GetViewportDIB() 109 | setclipboardbitmap grab 110 | close grab''' 111 | mxs_eval(args) -------------------------------------------------------------------------------- /Examples/simplewebserver.py: -------------------------------------------------------------------------------- 1 | import BaseHTTPServer 2 | import cgi 3 | import ctypes 4 | import os 5 | import sys 6 | import threading 7 | 8 | from PySide import QtGui 9 | 10 | import MaxPlus 11 | 12 | PORT = 8000 13 | 14 | 15 | class MyThread(threading.Thread): 16 | def __init__(self): 17 | threading.Thread.__init__(self) 18 | self.exiting = False 19 | 20 | address = ('localhost', PORT) 21 | self.server = BaseHTTPServer.HTTPServer(address, MyHandler) 22 | self._stop = threading.Event() 23 | 24 | def run(self): 25 | self.server.serve_forever() 26 | 27 | def stop(self): 28 | self.server.server_close() 29 | self.server.shutdown() 30 | self._stop.set() 31 | 32 | 33 | 34 | class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): 35 | def do_GET(self): 36 | rootdir = os.path.join(os.path.dirname(__file__) + '/html') 37 | try: 38 | if self.path == '/': 39 | self.path = '/index.html' 40 | if self.path.endswith('.html'): 41 | 42 | self.send_response(200) 43 | self.send_header('Content-type','text-html') 44 | self.end_headers() 45 | 46 | f = open(rootdir + self.path) 47 | self.wfile.write(f.read()) 48 | f.close() 49 | return 50 | 51 | except IOError: 52 | self.send_error(404, 'file not found') 53 | 54 | def do_POST(self): 55 | if self.path=="/cmd": 56 | form = cgi.FieldStorage( 57 | fp=self.rfile, 58 | headers=self.headers, 59 | environ={'REQUEST_METHOD':'POST', 60 | 'CONTENT_TYPE':self.headers['Content-Type'], 61 | }) 62 | 63 | self.send_response(301) 64 | self.send_header('Location', '/') 65 | self.end_headers() 66 | try: 67 | MaxPlus.Core.EvalMAXScript(form["cmd"].value) 68 | MaxPlus.ViewportManager_ForceCompleteRedraw() 69 | except: 70 | print "Needs to be run from a 3ds max instance" 71 | return 72 | 73 | 74 | class MyWindow(QtGui.QWidget): 75 | def __init__(self, parent=None): 76 | super(MyWindow, self).__init__(parent) 77 | self.setWindowTitle('Simple 3ds Max webserver') 78 | self.resize(200,50) 79 | self.btn_run = QtGui.QPushButton('Run') 80 | layout = QtGui.QVBoxLayout() 81 | layout.addWidget(self.btn_run) 82 | self.setLayout(layout) 83 | self.btn_run.clicked.connect(self.run) 84 | 85 | self.serverThread = None 86 | 87 | def run(self): 88 | if not self.serverThread: 89 | print "Serving at port", PORT 90 | self.btn_run.setText('Stop...') 91 | self.serverThread = MyThread() 92 | self.serverThread.start() 93 | else: 94 | print "Stopping webserver" 95 | self.btn_run.setText('Run') 96 | self.serverThread.stop() 97 | self.serverThread = None 98 | 99 | def closeEvent(self, *args, **kwargs): 100 | if self.serverThread: 101 | print "Stopping webserver" 102 | self.btn_run.setText('Run') 103 | self.serverThread.stop() 104 | self.serverThread = None 105 | 106 | class _GCProtector(object): 107 | controls = [] 108 | 109 | 110 | if __name__ == '__main__': 111 | app = QtGui.QApplication.instance() 112 | if not app: 113 | app = QtGui.QApplication([]) 114 | 115 | window = MyWindow() 116 | _GCProtector.controls.append(window) 117 | window.show() 118 | 119 | capsule = window.effectiveWinId() 120 | ctypes.pythonapi.PyCObject_AsVoidPtr.restype = ctypes.c_void_p 121 | ctypes.pythonapi.PyCObject_AsVoidPtr.argtypes = [ctypes.py_object] 122 | ptr = ctypes.pythonapi.PyCObject_AsVoidPtr(capsule) 123 | 124 | MaxPlus.Win32.Set3dsMaxAsParentWindow(ptr) 125 | 126 | -------------------------------------------------------------------------------- /Examples/ui/editor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 936 10 | 750 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 0 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 32 | 33 | true 34 | 35 | 36 | QTabWidget::North 37 | 38 | 39 | QTabWidget::Rounded 40 | 41 | 42 | 0 43 | 44 | 45 | true 46 | 47 | 48 | true 49 | 50 | 51 | true 52 | 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 0 60 | 0 61 | 62 | 63 | 64 | false 65 | 66 | 67 | New 68 | 69 | 70 | 71 | 0 72 | 73 | 74 | 0 75 | 76 | 77 | 0 78 | 79 | 80 | 0 81 | 82 | 83 | 0 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | border:0 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 0 108 | 0 109 | 936 110 | 18 111 | 112 | 113 | 114 | 115 | File 116 | 117 | 118 | 119 | 120 | 121 | 122 | Tools 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | Open 134 | 135 | 136 | 137 | 138 | Exit 139 | 140 | 141 | 142 | 143 | Run 144 | 145 | 146 | 147 | 148 | Clear Listener 149 | 150 | 151 | 152 | 153 | 154 | QsciScintilla 155 | QFrame 156 |
Qsci/qsciscintilla.h
157 | 1 158 |
159 |
160 | 161 | 162 | 163 | actionExit 164 | triggered() 165 | MainWindow 166 | close() 167 | 168 | 169 | -1 170 | -1 171 | 172 | 173 | 469 174 | 374 175 | 176 | 177 | 178 | 179 |
180 | -------------------------------------------------------------------------------- /Examples/pyqt_pyeditor.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt4 import QtGui, uic, QtCore 3 | from PyQt4.Qsci import QsciScintilla, QsciLexerPython 4 | 5 | import MaxPlus 6 | 7 | 8 | def getscriptpath(): 9 | pkgdir = os.path.dirname(os.path.abspath(__file__)) 10 | return pkgdir 11 | 12 | PACKAGEDIR = getscriptpath() 13 | 14 | MainWindowForm, MainWindowBase = uic.loadUiType( 15 | os.path.join(getscriptpath(), 'ui', 'editor.ui')) 16 | 17 | 18 | def formatstringtomaxscript(code): 19 | code = code.replace('\"', '\'') 20 | code = code.replace('\\', '\\\\') 21 | res = 'python.Execute("%s")' % code 22 | return res 23 | 24 | 25 | class MainWindow(MainWindowBase, MainWindowForm): 26 | def __init__(self, parent=None): 27 | super(MainWindow, self).__init__(parent) 28 | 29 | self.setupUi(self) 30 | self.font = QtGui.QFont() 31 | self.setupui(self.textEdit, self.font) 32 | self.lexer = QsciLexerPython() 33 | 34 | self.textEdit.setLexer(self.lexer) 35 | self.textEdit.setText(r'''import MaxPlus 36 | # Look in the MAXScript listener 37 | MaxPlus.Core.WriteLine("hello world")''') 38 | 39 | self.btn_run.triggered.connect(self.editorrun) 40 | self.btn_clear.triggered.connect(self.editorclear) 41 | self.actionOpen.triggered.connect(self.loadfile) 42 | 43 | self.connect(self.tabWidget, QtCore.SIGNAL('tabCloseRequested(int)'), self.closetab) 44 | 45 | 46 | def setupui(self, editor, font): 47 | ## define the font to use 48 | 49 | font.setFamily('Consolas') 50 | font.setFixedPitch(True) 51 | font.setPointSize(10) 52 | # the font metrics here will help 53 | # building the margin width later 54 | fm = QtGui.QFontMetrics(font) 55 | 56 | ## set the default font of the editor 57 | ## and take the same font for line numbers 58 | editor.setFont(font) 59 | editor.setMarginsFont(font) 60 | 61 | ## Line numbers 62 | # conventionnaly, margin 0 is for line numbers 63 | editor.setMarginWidth(0, fm.width('0000')) 64 | 65 | ## Edge Mode shows a red vetical bar at 80 chars 66 | editor.setEdgeMode(QsciScintilla.EdgeLine) 67 | editor.setEdgeColumn(80) 68 | editor.setEdgeColor(QtGui.QColor('LightGray')) 69 | 70 | ## Folding visual : we will use boxes 71 | editor.setFolding(QsciScintilla.BoxedTreeFoldStyle) 72 | 73 | ## Braces matching 74 | editor.setBraceMatching(QsciScintilla.SloppyBraceMatch) 75 | 76 | ## Editing line color 77 | editor.setCaretLineVisible(True) 78 | editor.setCaretLineBackgroundColor(QtGui.QColor('LightYellow')) 79 | 80 | ## Margins colors 81 | # line numbers margin 82 | editor.setMarginsBackgroundColor(QtGui.QColor('LightGray')) 83 | editor.setMarginsForegroundColor(QtGui.QColor('DarkRed')) 84 | 85 | # folding margin colors (foreground,background) 86 | editor.setFoldMarginColors(QtGui.QColor('Lime'),QtGui.QColor('White')) 87 | 88 | def newtab(self, tw, tname): 89 | newtab = QtGui.QWidget() 90 | vlayout = QtGui.QVBoxLayout() 91 | vlayout.setMargin(0) 92 | 93 | neweditor = QsciScintilla(newtab) 94 | neweditor.setStyleSheet("border:0") 95 | self.setupui(neweditor, self.font) 96 | neweditor.setLexer(self.lexer) 97 | 98 | vlayout.addWidget(neweditor) 99 | newtab.setLayout(vlayout) 100 | 101 | return tw.addTab(newtab, os.path.basename(str(tname))), neweditor 102 | 103 | def loadfile(self): 104 | fname = QtGui.QFileDialog.getOpenFileName( 105 | self, 'Open file', os.path.join(PACKAGEDIR, '..\Examples')) 106 | 107 | f = open(fname, 'r') 108 | with f: 109 | idx, neweditor = self.newtab(self.tabWidget, os.path.basename(str(fname))) 110 | self.tabWidget.setCurrentIndex(idx) 111 | data = f.read() 112 | neweditor.setText(data) 113 | 114 | def closetab(self, idx): 115 | if self.tabWidget.count() != 1: 116 | self.tabWidget.removeTab(idx) 117 | 118 | def gettabtext(self): 119 | ctab = self.tabWidget.currentWidget() 120 | for idx, child in enumerate(ctab.children()): 121 | if type(child) is type(QsciScintilla()): 122 | return child.text() 123 | 124 | def editorrun(self): 125 | pycode = str(self.gettabtext()) 126 | MaxPlus.Core.EvalMAXScript(formatstringtomaxscript(pycode)) 127 | 128 | def editorclear(self): 129 | MaxPlus.Core.EvalMAXScript('clearListener()') 130 | 131 | 132 | class _FocusFilter(QtCore.QObject): 133 | """ Used to filter events to properly manage focus in 3ds Max. This is a hack to deal with the fact 134 | that mixing Qt and Win32 causes focus events to not get triggered as expected. """ 135 | 136 | def eventFilter(self, obj, event): 137 | if event.type() == QtCore.QEvent.MouseButtonPress: 138 | MaxPlus.CUI.DisableAccelerators() 139 | return False 140 | 141 | 142 | if __name__ == '__main__': 143 | app = QtGui.QApplication.instance() 144 | if not app: 145 | app = QtGui.QApplication([]) 146 | 147 | # Install filter so we can disable 3dsMax accelerators everytime we focus on our Script Editor 148 | filter = _FocusFilter() 149 | app.installEventFilter(filter) 150 | 151 | window = MainWindow() 152 | window.show() 153 | -------------------------------------------------------------------------------- /Externals/Npp/userDefineLang.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 03/* 03 04*/ 04 00-- 01 02 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ! " * . \ _ + < = > 17 | 18 | ( 19 | 20 | ) 21 | ( 22 | 23 | ) 24 | /* 25 | 26 | */ 27 | and as at by case catch continue do else exit fn for from function global if in local macroscript mapped not of off on or random return select set then throw to try when where while with 28 | rollout subRollout slider button spinner checkbox checkButton pickButton radioButtons activeXControl messagebox querybox yesNoCancelBox disableSceneRedraw enableSceneRedraw forceCompleteRedraw redrawViews rcmenu createDialog destroyDialog groupBox label editText closeRolloutFloater addRollout removeRollout dropDownList listBox multiListBox colorPicker timer progressBar open entered changed selected pressed selectionEnd doubleClicked picked clicked buttondown buttonup tick resized mousedown mouseup click dblclick mouseover mouseout selChanged ptChanged tangentChanged deleted reset ColumnClick 29 | append collect findItem join delete group deleteItem loadMaxFile getMaxOpenFilename getFilenameFile fileProperties close openFile include getSavePath createFile resetMaxFile getOpenFilename getSaveFileName skipToString eof seek skipToString readLine skipToNextLine filePos flush readValue readExpr execute deleteFile readChar readChars readDelimitedString doesFileExist mergeMaxFile saveMaxFile exportFile getFiles renameFile maxVersion fetchMaxFile holdMaxFile enumerateFiles classOf subAnim material textureMap numSubs isKindOf superClassOf freeSceneBitmaps getMeditMaterial getNumSubMtls animate coordsys cloneNodes intersectRay isGroupHead isGroupMember isValidNode maxOps rotate scale spaceWarpObject sliderTime filterString stringStream format print getFilenamePath findString replace subString filenameFromPath getFileModDate readDelimitedString trimLeft trimRight openLog flushLog closeLog clearListener 30 | about parameters persistent plugin struct tool undo utility DOSCommand floor collapseStack saveNodes gc addModifier timeStamp localTime makeDir actionMan undefined unsupplied 31 | 32 | 33 | 34 | 35 | 00" 01\ 02" 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /packages/maxrenderelements/RenderElementManager.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on 07/11/2013 3 | 4 | @author: Daniel Santana 5 | ''' 6 | import MaxPlus 7 | 8 | mxs_eval = MaxPlus.Core.EvalMAXScript 9 | 10 | 11 | class RenderElement(object): 12 | ''' 13 | Single Render Element 14 | We could use the Animatable return by the GetRenderElement, but since we can't pass 15 | it to MAXScript, let's leave for now as is. 16 | ''' 17 | _internal_index = -1 18 | __mxs_get_file = '((maxOps.GetCurRenderElementMgr()).GetRenderElementFilename %i) as string' 19 | __mxs_set_file = '(maxOps.GetCurRenderElementMgr()).SetRenderElementFilename %i "%s"' 20 | __mxs_get_element = 'el = (maxOps.GetCurRenderElementMgr()).GetRenderElement %i;' 21 | __mxs_fn1 = 'dsObjectPropertiesHelper._set_property %s #%s %s;' 22 | __mxs_fn2 = 'dsObjectPropertiesHelper._get_property %s #%s;' 23 | __mxs_fn3 = 'dsObjectPropertiesHelper != undefined' 24 | __mxs_fn4 = 'dsObjectPropertiesHelper._has_property %s #%s;' 25 | __mxs_fns = '''global dsObjectPropertiesHelper 26 | -- Some parts are based on pen PresetManager for type checking 27 | struct dsObjectPropertiesHelper 28 | ( 29 | fn isCompatable valueClass keyClass= 30 | ( 31 | case of 32 | ( 33 | (valueClass==keyClass): true 34 | (valueClass==float and keyClass==integer): true 35 | (valueClass==integer and keyClass==float): true 36 | (valueClass==filename and keyClass==string): true 37 | (valueClass==string and keyClass==filename): true 38 | default: false 39 | ) 40 | ), 41 | fn _set_property obj k value = 42 | ( 43 | local classString=((classOf obj) as string) 44 | if not hasProperty obj k do return False 45 | local keyClass=(classOf value) 46 | local valueClass=(classOf (getProperty obj k)) 47 | if isCompatable valueClass keyClass then 48 | setProperty obj k value 49 | else 50 | format "Warning: Values are not compatable for \\"%\\"\\n\\tProperty \\"%\\" needs value class of % got %\\n" classString k valueClass keyClass 51 | ), 52 | fn _get_property obj k = 53 | ( 54 | if not hasProperty obj k do return False 55 | r = getProperty obj k 56 | c = classof r 57 | if c == filename do return r as string 58 | if r == undefined do r = "" 59 | r 60 | ), 61 | fn _has_property obj k = 62 | ( 63 | hasProperty obj k 64 | ) 65 | ) 66 | dsObjectPropertiesHelper = dsObjectPropertiesHelper() 67 | True''' 68 | 69 | def __init__(self, n=-1, className=None): 70 | self._internal_index = n 71 | 72 | def __init_mxs_(self): 73 | if not mxs_eval(self.__mxs_fn3).Get(): 74 | mxs_eval(self.__mxs_fns) 75 | 76 | def __getattr__(self, name): 77 | if name in ['_internal_index']: 78 | raise AttributeError 79 | if name == 'File': 80 | return self._get_filename() 81 | self.__init_mxs_() 82 | mx_element = self.__mxs_get_element % self._internal_index 83 | mx_script = mx_element + (self.__mxs_fn4 % ('el', name)) 84 | if mxs_eval(mx_script).Get(): 85 | mx_script = mx_element + (self.__mxs_fn2 % ('el', name)) 86 | return mxs_eval(mx_script).Get() 87 | else: 88 | print "Element does't contain '%s'" % name 89 | raise AttributeError 90 | 91 | def __setattr__(self, name, value): 92 | if name in ['_internal_index']: 93 | object.__setattr__(self, name, value) 94 | return 95 | if name == 'File': 96 | self._set_filename(name) 97 | return 98 | self.__init_mxs_() 99 | mx_element = self.__mxs_get_element % self._internal_index 100 | mx_script = mx_element + (self.__mxs_fn4 % ('el', name)) 101 | if mxs_eval(mx_script).Get(): 102 | mx_script = mx_element + (self.__mxs_fn1 % ('el', name, value)) 103 | mxs_eval(mx_script) 104 | else: 105 | print "Couldn't seet element property '%s' with value %s" % (name, value) 106 | raise AttributeError 107 | 108 | def _set_filename(self, name): 109 | mxs_eval(self.__mxs_set_file % (self._internal_index, name)) 110 | 111 | def _get_filename(self): 112 | return mxs_eval(self.__mxs_get_file % self._internal_index).Get() 113 | 114 | 115 | class RenderElementManager(object): 116 | ''' 117 | Missing RenderElementManager class 118 | ''' 119 | 120 | __mxs_re_count = '(maxOps.GetCurRenderElementMgr()).NumRenderElements()' 121 | __mxs_re_del = '(maxOps.GetCurRenderElementMgr()).RemoveAllElements()' 122 | __mxs_re_add = '''elm = %s() 123 | (maxOps.GetCurRenderElementMgr()).AddRenderElement elm''' 124 | __mxs_re_g_active = '(maxOps.GetCurRenderElementMgr()).GetElementsActive()' 125 | __mxs_re_s_active = '(maxOps.GetCurRenderElementMgr()).SetElementsActive %s' 126 | __mxs_re_g_display = '(maxOps.GetCurRenderElementMgr()).GetDisplayElements()' 127 | __mxs_re_s_display = '(maxOps.GetCurRenderElementMgr()).SetDisplayElements %s' 128 | 129 | def __init__(self): 130 | pass 131 | 132 | def __build_element(self, i): 133 | return RenderElement(i) 134 | 135 | def AddElement(self, className): 136 | ''' 137 | Adds a new render element using the class name and 138 | returns it. 139 | @param className: Name of the element to add (MAXScript class name) 140 | @type className: str 141 | @return: Element 142 | @rtype: RenderElement 143 | ''' 144 | if mxs_eval(self.__mxs_re_add % className).Get(): 145 | return self.__build_element(len(self)) 146 | return None 147 | 148 | #def RemoveElement(self, className): 149 | # pass 150 | 151 | def RemoveAllElements(self): 152 | return mxs_eval(self.__mxs_re_del) 153 | 154 | def GetElements(self): 155 | return [self.__build_element(i) for i in xrange(len(self))] 156 | 157 | def __getitem__(self, i): 158 | return self.__build_element(i) 159 | 160 | def __len__(self): 161 | return mxs_eval(self.__mxs_re_count).Get() 162 | 163 | @property 164 | def Active(self): 165 | return mxs_eval(self.__mxs_re_g_active).Get() 166 | 167 | @Active.setter 168 | def Active(self, value): 169 | mxs_eval(self.__mxs_re_s_active % value) 170 | 171 | @property 172 | def Display(self): 173 | return mxs_eval(self.__mxs_re_g_display).Get() 174 | 175 | @Display.setter 176 | def Display(self, value): 177 | mxs_eval(self.__mxs_re_s_display % value) 178 | 179 | Elements = property(lambda self: (self[i] for i in xrange(len(self)))) 180 | 181 | # Adding a new class to MaxPlus 182 | MaxPlus.RenderElementManager = RenderElementManager 183 | 184 | if __name__ == '__main__': 185 | re = RenderElementManager() 186 | print 'Number of render elements %s' % len(re) 187 | for element in re.Elements: 188 | # NOTE: Please change this, since i was testing with VRay elements 189 | print 'Element %s is %s' % (element.ElementName, 'enabled' if element.VrayVFB else 'disabled') 190 | element.VrayVFB = False 191 | print 'Element %s is now %s' % (element.ElementName, 'enabled' if element.VrayVFB else 'disabled') 192 | print 'Filename=', element.File 193 | -------------------------------------------------------------------------------- /Externals/FB_oAuth/facebook.php: -------------------------------------------------------------------------------- 1 | initSharedSession(); 64 | 65 | // re-load the persisted state, since parent 66 | // attempted to read out of non-shared cookie 67 | $state = $this->getPersistentData('state'); 68 | if (!empty($state)) { 69 | $this->state = $state; 70 | } else { 71 | $this->state = null; 72 | } 73 | 74 | } 75 | } 76 | 77 | /** 78 | * Supported keys for persistent data 79 | * 80 | * @var array 81 | */ 82 | protected static $kSupportedKeys = 83 | array('state', 'code', 'access_token', 'user_id'); 84 | 85 | /** 86 | * Initiates Shared Session 87 | */ 88 | protected function initSharedSession() { 89 | $cookie_name = $this->getSharedSessionCookieName(); 90 | if (isset($_COOKIE[$cookie_name])) { 91 | $data = $this->parseSignedRequest($_COOKIE[$cookie_name]); 92 | if ($data && !empty($data['domain']) && 93 | self::isAllowedDomain($this->getHttpHost(), $data['domain'])) { 94 | // good case 95 | $this->sharedSessionID = $data['id']; 96 | return; 97 | } 98 | // ignoring potentially unreachable data 99 | } 100 | // evil/corrupt/missing case 101 | $base_domain = $this->getBaseDomain(); 102 | $this->sharedSessionID = md5(uniqid(mt_rand(), true)); 103 | $cookie_value = $this->makeSignedRequest( 104 | array( 105 | 'domain' => $base_domain, 106 | 'id' => $this->sharedSessionID, 107 | ) 108 | ); 109 | $_COOKIE[$cookie_name] = $cookie_value; 110 | if (!headers_sent()) { 111 | $expire = time() + self::FBSS_COOKIE_EXPIRE; 112 | setcookie($cookie_name, $cookie_value, $expire, '/', '.'.$base_domain); 113 | } else { 114 | // @codeCoverageIgnoreStart 115 | self::errorLog( 116 | 'Shared session ID cookie could not be set! You must ensure you '. 117 | 'create the Facebook instance before headers have been sent. This '. 118 | 'will cause authentication issues after the first request.' 119 | ); 120 | // @codeCoverageIgnoreEnd 121 | } 122 | } 123 | 124 | /** 125 | * Provides the implementations of the inherited abstract 126 | * methods. The implementation uses PHP sessions to maintain 127 | * a store for authorization codes, user ids, CSRF states, and 128 | * access tokens. 129 | */ 130 | 131 | /** 132 | * {@inheritdoc} 133 | * 134 | * @see BaseFacebook::setPersistentData() 135 | */ 136 | protected function setPersistentData($key, $value) { 137 | if (!in_array($key, self::$kSupportedKeys)) { 138 | self::errorLog('Unsupported key passed to setPersistentData.'); 139 | return; 140 | } 141 | 142 | $session_var_name = $this->constructSessionVariableName($key); 143 | $_SESSION[$session_var_name] = $value; 144 | } 145 | 146 | /** 147 | * {@inheritdoc} 148 | * 149 | * @see BaseFacebook::getPersistentData() 150 | */ 151 | protected function getPersistentData($key, $default = false) { 152 | if (!in_array($key, self::$kSupportedKeys)) { 153 | self::errorLog('Unsupported key passed to getPersistentData.'); 154 | return $default; 155 | } 156 | 157 | $session_var_name = $this->constructSessionVariableName($key); 158 | return isset($_SESSION[$session_var_name]) ? 159 | $_SESSION[$session_var_name] : $default; 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | * 165 | * @see BaseFacebook::clearPersistentData() 166 | */ 167 | protected function clearPersistentData($key) { 168 | if (!in_array($key, self::$kSupportedKeys)) { 169 | self::errorLog('Unsupported key passed to clearPersistentData.'); 170 | return; 171 | } 172 | 173 | $session_var_name = $this->constructSessionVariableName($key); 174 | if (isset($_SESSION[$session_var_name])) { 175 | unset($_SESSION[$session_var_name]); 176 | } 177 | } 178 | 179 | /** 180 | * {@inheritdoc} 181 | * 182 | * @see BaseFacebook::clearAllPersistentData() 183 | */ 184 | protected function clearAllPersistentData() { 185 | foreach (self::$kSupportedKeys as $key) { 186 | $this->clearPersistentData($key); 187 | } 188 | if ($this->sharedSessionID) { 189 | $this->deleteSharedSessionCookie(); 190 | } 191 | } 192 | 193 | /** 194 | * Deletes Shared session cookie 195 | */ 196 | protected function deleteSharedSessionCookie() { 197 | $cookie_name = $this->getSharedSessionCookieName(); 198 | unset($_COOKIE[$cookie_name]); 199 | $base_domain = $this->getBaseDomain(); 200 | setcookie($cookie_name, '', 1, '/', '.'.$base_domain); 201 | } 202 | 203 | /** 204 | * Returns the Shared session cookie name 205 | * 206 | * @return string The Shared session cookie name 207 | */ 208 | protected function getSharedSessionCookieName() { 209 | return self::FBSS_COOKIE_NAME . '_' . $this->getAppId(); 210 | } 211 | 212 | /** 213 | * Constructs and returns the name of the session key. 214 | * 215 | * @see setPersistentData() 216 | * @param string $key The key for which the session variable name to construct. 217 | * 218 | * @return string The name of the session key. 219 | */ 220 | protected function constructSessionVariableName($key) { 221 | $parts = array('fb', $this->getAppId(), $key); 222 | if ($this->sharedSessionID) { 223 | array_unshift($parts, $this->sharedSessionID); 224 | } 225 | return implode('_', $parts); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /packages/maxconnect/tomax.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Taken from Sublime3dsmax : http://cbuelter.de/?p=535 3 | ''' 4 | ############################################################################ 5 | # 6 | # This module finds 3ds Max and the MAXScript Listener and can 7 | # send strings and button strokes to it. 8 | # 9 | # Completely based on m2u: http://alfastuff.wordpress.com/2013/10/13/m2u/ 10 | # and figured out by the amazing Johannes: http://alfastuff.wordpress.com/ 11 | # 12 | # Known issues: EnumPos for childwindows changes, 13 | # e.g. if using create mode or hierarchy mode. 14 | # Current workaround is to use the first handle 15 | # that matches cls="MXS_Scintilla", which is the 16 | # mini macro recorder, to paste text into. 17 | # 18 | ############################################################################ 19 | 20 | # keeps all the required UI elements of the Max and talks to them 21 | import ctypes #required for windows ui stuff 22 | import threading 23 | 24 | MAX_TITLE_IDENTIFIER = r"Autodesk 3ds Max" 25 | 26 | # UI element window handles 27 | gMaxThreadProcessID = None 28 | gMainWindow = None 29 | gMiniMacroRecorder = None 30 | 31 | # windows functions and constants 32 | # stuff for finding and analyzing UI Elements 33 | EnumWindows = ctypes.windll.user32.EnumWindows 34 | EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)) 35 | EnumChildWindows = ctypes.windll.user32.EnumChildWindows 36 | FindWindowEx = ctypes.windll.user32.FindWindowExW 37 | 38 | GetClassName = ctypes.windll.user32.GetClassNameW 39 | GetWindowText = ctypes.windll.user32.GetWindowTextW 40 | GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW 41 | IsWindowVisible = ctypes.windll.user32.IsWindowVisible 42 | GetWindow = ctypes.windll.user32.GetWindow 43 | GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId 44 | 45 | PostMessage = ctypes.windll.user32.PostMessageA 46 | SendMessage = ctypes.windll.user32.SendMessageA 47 | 48 | WM_SETTEXT = 0x000C 49 | WM_KEYDOWN = 0x0100 50 | WM_KEYUP = 0x0101 51 | WM_CHAR = 0x0102 # the alternative to WM_KEYDOWN 52 | VK_RETURN = 0x0D # Enter key 53 | 54 | # attaching is required for SendMessage and the like to actually work like it should 55 | AttachThreadInput = ctypes.windll.user32.AttachThreadInput 56 | 57 | class ThreadWinLParm(ctypes.Structure): 58 | """lParam object to get a name to and an object back from a windows 59 | enumerator function. 60 | 61 | .. seealso:: :func:`_getChildWindowByName` 62 | """ 63 | _fields_=[ 64 | ("name", ctypes.c_wchar_p), 65 | ("cls", ctypes.c_wchar_p), 66 | ("hwnd", ctypes.POINTER(ctypes.c_long)), 67 | ("enumPos", ctypes.c_int), 68 | ("_enum", ctypes.c_int) # keep track of current enum step 69 | ] 70 | 71 | def _getChildWindowByName(hwnd, lParam): 72 | """callback function to be called by EnumChildWindows, see 73 | :func:`getChildWindowByName` 74 | 75 | :param hwnd: the window handle 76 | :param lParam: a :ref:`ctypes.byref` instance of :class:`ThreadWinLParam` 77 | 78 | if name is None, the cls name is taken, 79 | if cls is None, the name is taken, 80 | if both are None, all elements are printed 81 | if both have values, only the element matching both will fit 82 | 83 | """ 84 | length = GetWindowTextLength(hwnd) 85 | buff = ctypes.create_unicode_buffer(length + 1) 86 | GetWindowText(hwnd, buff, length + 1) 87 | param = ctypes.cast(lParam, ctypes.POINTER(ThreadWinLParm)).contents 88 | param._enum += 1 89 | 90 | length = 255 91 | cbuff = ctypes.create_unicode_buffer(length + 1) 92 | GetClassName(hwnd, cbuff, length+1) 93 | if param.name == None and param.cls != None: 94 | if param.cls in cbuff.value:# == param.cls: 95 | param.hwnd = hwnd 96 | return False 97 | elif param.cls == None and param.name != None: 98 | if buff.value == param.name: 99 | param.hwnd = hwnd 100 | return False 101 | elif param.cls != None and param.name != None: 102 | if buff.value == param.name and param.cls in cbuff.value:# == param.cls: 103 | param.hwnd = hwnd 104 | return False 105 | else: #both values are None, print the current element 106 | print ("wnd cls: "+cbuff.value+" name: "+buff.value+" enum: "+str(param._enum)) 107 | return True 108 | 109 | def getChildWindowByName(hwnd, name = None, cls = None): 110 | """find a window by its name or clsName, returns the window's hwnd 111 | 112 | :param hwnd: the parent window's hwnd 113 | :param name: the name/title to search for 114 | :param cls: the clsName to search for 115 | 116 | :return: the hwnd of the matching child window 117 | 118 | if name is None, the cls name is taken, 119 | if cls is None, the name is taken, 120 | if both are None, all elements are printed 121 | if both have values, only the element matching both will fit. 122 | 123 | .. seealso:: :func:`_getChildWindowByName`, :func:`getChildWindowByEnumPos` 124 | 125 | """ 126 | param = ThreadWinLParm(name=name,cls=cls,_enum=-1) 127 | lParam = ctypes.byref(param) 128 | EnumChildWindows(hwnd, EnumWindowsProc(_getChildWindowByName),lParam) 129 | return param.hwnd 130 | 131 | def getMXSMiniMacroRecorder(): 132 | """convenience function 133 | """ 134 | # The function will return the first param that matches the class name. 135 | # Thankfully, this is the MAXScript Mini Listener. 136 | global gMainWindow 137 | miniMacroRecorderHandle = getChildWindowByName(gMainWindow, name=None, cls="MXS_Scintilla") 138 | return miniMacroRecorderHandle 139 | 140 | def _getChildWindowByEnumPos(hwnd, lParam): 141 | """ callback function, see :func:`getChildWindowByEnumPos` """ 142 | param = ctypes.cast(lParam, ctypes.POINTER(ThreadWinLParm)).contents 143 | param._enum += 1 144 | if param._enum == param.enumPos: 145 | param.hwnd = hwnd 146 | return False 147 | return True 148 | 149 | def getChildWindowByEnumPos(hwnd, pos): 150 | """get a child window by its enum pos, return its hwnd 151 | 152 | :param hwnd: the parent window's hwnd 153 | :param pos: the number to search for 154 | 155 | :return: the hwnd of the matching child window 156 | 157 | This function uses the creation order which is reflected in Windows Enumerate 158 | functions to get the handle to a certain window. This is useful when the 159 | name or cls of the desired window is not unique or not given. 160 | 161 | You can count the enum pos by printing all child windows of a window. 162 | .. seealso:: :func:`getChildWindowByName` 163 | 164 | """ 165 | param = ThreadWinLParm(name = None, cls = None, enumPos = pos, _enum = -1) 166 | EnumChildWindows( hwnd, EnumWindowsProc(_getChildWindowByEnumPos), ctypes.byref(param)) 167 | return param.hwnd 168 | 169 | def attachThreads(hwnd): 170 | """tell Windows to attach the program and the max threads. 171 | 172 | This will give us some benefits in control, for example SendMessage calls to 173 | the max thread will only return when Max has processed the message, amazing! 174 | 175 | """ 176 | thread = GetWindowThreadProcessId(hwnd, 0) #max thread 177 | global gMaxThreadProcessID 178 | gMaxThreadProcessID = thread 179 | thisThread = threading.current_thread().ident #program thread 180 | AttachThreadInput(thread, thisThread, True) 181 | 182 | def _getWindows(hwnd, lParam): 183 | """callback function, find the Max Window (and fill the ui element vars) 184 | 185 | This is a callback function. Windows itself will call this function for 186 | every top-level window in EnumWindows iterator function. 187 | .. seealso:: :func:`connectToMax` 188 | """ 189 | if IsWindowVisible(hwnd): 190 | length = GetWindowTextLength(hwnd) 191 | buff = ctypes.create_unicode_buffer(length + 1) 192 | GetWindowText(hwnd, buff, length + 1) 193 | global MAX_TITLE_IDENTIFIER 194 | if MAX_TITLE_IDENTIFIER in buff.value: 195 | global gMainWindow, gMaxThreadProcessID 196 | gMainWindow = hwnd 197 | attachThreads(gMainWindow) 198 | 199 | # Find MAXScript Mini Listener 200 | global gMiniMacroRecorder 201 | gMiniMacroRecorder = getMXSMiniMacroRecorder() 202 | return False 203 | return True 204 | 205 | def connectToMax(): 206 | global gMainWindow 207 | EnumWindows(EnumWindowsProc(_getWindows), 0) 208 | return (gMainWindow is not None) 209 | 210 | def fireCommand(command): 211 | """Executes the command string in Max. 212 | ';' at end needed for ReturnKey to be accepted.""" 213 | global gMiniMacroRecorder 214 | SendMessage(gMiniMacroRecorder, WM_SETTEXT, 0, str(command) ) 215 | SendMessage(gMiniMacroRecorder, WM_CHAR, VK_RETURN, 0) 216 | 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 2013 arturleao 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Externals/FB_oAuth/base_facebook.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class FacebookApiException extends Exception 31 | { 32 | /** 33 | * The result from the API server that represents the exception information. 34 | * 35 | * @var mixed 36 | */ 37 | protected $result; 38 | 39 | /** 40 | * Make a new API Exception with the given result. 41 | * 42 | * @param array $result The result from the API server 43 | */ 44 | public function __construct($result) { 45 | $this->result = $result; 46 | 47 | $code = 0; 48 | if (isset($result['error_code']) && is_int($result['error_code'])) { 49 | $code = $result['error_code']; 50 | } 51 | 52 | if (isset($result['error_description'])) { 53 | // OAuth 2.0 Draft 10 style 54 | $msg = $result['error_description']; 55 | } else if (isset($result['error']) && is_array($result['error'])) { 56 | // OAuth 2.0 Draft 00 style 57 | $msg = $result['error']['message']; 58 | } else if (isset($result['error_msg'])) { 59 | // Rest server style 60 | $msg = $result['error_msg']; 61 | } else { 62 | $msg = 'Unknown Error. Check getResult()'; 63 | } 64 | 65 | parent::__construct($msg, $code); 66 | } 67 | 68 | /** 69 | * Return the associated result object returned by the API server. 70 | * 71 | * @return array The result from the API server 72 | */ 73 | public function getResult() { 74 | return $this->result; 75 | } 76 | 77 | /** 78 | * Returns the associated type for the error. This will default to 79 | * 'Exception' when a type is not available. 80 | * 81 | * @return string 82 | */ 83 | public function getType() { 84 | if (isset($this->result['error'])) { 85 | $error = $this->result['error']; 86 | if (is_string($error)) { 87 | // OAuth 2.0 Draft 10 style 88 | return $error; 89 | } else if (is_array($error)) { 90 | // OAuth 2.0 Draft 00 style 91 | if (isset($error['type'])) { 92 | return $error['type']; 93 | } 94 | } 95 | } 96 | 97 | return 'Exception'; 98 | } 99 | 100 | /** 101 | * To make debugging easier. 102 | * 103 | * @return string The string representation of the error 104 | */ 105 | public function __toString() { 106 | $str = $this->getType() . ': '; 107 | if ($this->code != 0) { 108 | $str .= $this->code . ': '; 109 | } 110 | return $str . $this->message; 111 | } 112 | } 113 | 114 | /** 115 | * Provides access to the Facebook Platform. This class provides 116 | * a majority of the functionality needed, but the class is abstract 117 | * because it is designed to be sub-classed. The subclass must 118 | * implement the four abstract methods listed at the bottom of 119 | * the file. 120 | * 121 | * @author Naitik Shah 122 | */ 123 | abstract class BaseFacebook 124 | { 125 | /** 126 | * Version. 127 | */ 128 | const VERSION = '3.2.3'; 129 | 130 | /** 131 | * Signed Request Algorithm. 132 | */ 133 | const SIGNED_REQUEST_ALGORITHM = 'HMAC-SHA256'; 134 | 135 | /** 136 | * Default options for curl. 137 | * 138 | * @var array 139 | */ 140 | public static $CURL_OPTS = array( 141 | CURLOPT_CONNECTTIMEOUT => 10, 142 | CURLOPT_RETURNTRANSFER => true, 143 | CURLOPT_TIMEOUT => 60, 144 | CURLOPT_USERAGENT => 'facebook-php-3.2', 145 | ); 146 | 147 | /** 148 | * List of query parameters that get automatically dropped when rebuilding 149 | * the current URL. 150 | * 151 | * @var array 152 | */ 153 | protected static $DROP_QUERY_PARAMS = array( 154 | 'code', 155 | 'state', 156 | 'signed_request', 157 | ); 158 | 159 | /** 160 | * Maps aliases to Facebook domains. 161 | * 162 | * @var array 163 | */ 164 | public static $DOMAIN_MAP = array( 165 | 'api' => 'https://api.facebook.com/', 166 | 'api_video' => 'https://api-video.facebook.com/', 167 | 'api_read' => 'https://api-read.facebook.com/', 168 | 'graph' => 'https://graph.facebook.com/', 169 | 'graph_video' => 'https://graph-video.facebook.com/', 170 | 'www' => 'https://www.facebook.com/', 171 | ); 172 | 173 | /** 174 | * The Application ID. 175 | * 176 | * @var string 177 | */ 178 | protected $appId; 179 | 180 | /** 181 | * The Application App Secret. 182 | * 183 | * @var string 184 | */ 185 | protected $appSecret; 186 | 187 | /** 188 | * The ID of the Facebook user, or 0 if the user is logged out. 189 | * 190 | * @var integer 191 | */ 192 | protected $user; 193 | 194 | /** 195 | * The data from the signed_request token. 196 | * 197 | * @var string 198 | */ 199 | protected $signedRequest; 200 | 201 | /** 202 | * A CSRF state variable to assist in the defense against CSRF attacks. 203 | * 204 | * @var string 205 | */ 206 | protected $state; 207 | 208 | /** 209 | * The OAuth access token received in exchange for a valid authorization 210 | * code. null means the access token has yet to be determined. 211 | * 212 | * @var string 213 | */ 214 | protected $accessToken = null; 215 | 216 | /** 217 | * Indicates if the CURL based @ syntax for file uploads is enabled. 218 | * 219 | * @var boolean 220 | */ 221 | protected $fileUploadSupport = false; 222 | 223 | /** 224 | * Indicates if we trust HTTP_X_FORWARDED_* headers. 225 | * 226 | * @var boolean 227 | */ 228 | protected $trustForwarded = false; 229 | 230 | /** 231 | * Indicates if signed_request is allowed in query parameters. 232 | * 233 | * @var boolean 234 | */ 235 | protected $allowSignedRequest = true; 236 | 237 | /** 238 | * Initialize a Facebook Application. 239 | * 240 | * The configuration: 241 | * - appId: the application ID 242 | * - secret: the application secret 243 | * - fileUpload: (optional) boolean indicating if file uploads are enabled 244 | * - allowSignedRequest: (optional) boolean indicating if signed_request is 245 | * allowed in query parameters or POST body. Should be 246 | * false for non-canvas apps. Defaults to true. 247 | * 248 | * @param array $config The application configuration 249 | */ 250 | public function __construct($config) { 251 | $this->setAppId($config['appId']); 252 | $this->setAppSecret($config['secret']); 253 | if (isset($config['fileUpload'])) { 254 | $this->setFileUploadSupport($config['fileUpload']); 255 | } 256 | if (isset($config['trustForwarded']) && $config['trustForwarded']) { 257 | $this->trustForwarded = true; 258 | } 259 | if (isset($config['allowSignedRequest']) 260 | && !$config['allowSignedRequest']) { 261 | $this->allowSignedRequest = false; 262 | } 263 | $state = $this->getPersistentData('state'); 264 | if (!empty($state)) { 265 | $this->state = $state; 266 | } 267 | } 268 | 269 | /** 270 | * Set the Application ID. 271 | * 272 | * @param string $appId The Application ID 273 | * 274 | * @return BaseFacebook 275 | */ 276 | public function setAppId($appId) { 277 | $this->appId = $appId; 278 | return $this; 279 | } 280 | 281 | /** 282 | * Get the Application ID. 283 | * 284 | * @return string the Application ID 285 | */ 286 | public function getAppId() { 287 | return $this->appId; 288 | } 289 | 290 | /** 291 | * Set the App Secret. 292 | * 293 | * @param string $apiSecret The App Secret 294 | * 295 | * @return BaseFacebook 296 | * @deprecated Use setAppSecret instead. 297 | * @see setAppSecret() 298 | */ 299 | public function setApiSecret($apiSecret) { 300 | $this->setAppSecret($apiSecret); 301 | return $this; 302 | } 303 | 304 | /** 305 | * Set the App Secret. 306 | * 307 | * @param string $appSecret The App Secret 308 | * 309 | * @return BaseFacebook 310 | */ 311 | public function setAppSecret($appSecret) { 312 | $this->appSecret = $appSecret; 313 | return $this; 314 | } 315 | 316 | /** 317 | * Get the App Secret. 318 | * 319 | * @return string the App Secret 320 | * 321 | * @deprecated Use getAppSecret instead. 322 | * @see getAppSecret() 323 | */ 324 | public function getApiSecret() { 325 | return $this->getAppSecret(); 326 | } 327 | 328 | /** 329 | * Get the App Secret. 330 | * 331 | * @return string the App Secret 332 | */ 333 | public function getAppSecret() { 334 | return $this->appSecret; 335 | } 336 | 337 | /** 338 | * Set the file upload support status. 339 | * 340 | * @param boolean $fileUploadSupport The file upload support status. 341 | * 342 | * @return BaseFacebook 343 | */ 344 | public function setFileUploadSupport($fileUploadSupport) { 345 | $this->fileUploadSupport = $fileUploadSupport; 346 | return $this; 347 | } 348 | 349 | /** 350 | * Get the file upload support status. 351 | * 352 | * @return boolean true if and only if the server supports file upload. 353 | */ 354 | public function getFileUploadSupport() { 355 | return $this->fileUploadSupport; 356 | } 357 | 358 | /** 359 | * Get the file upload support status. 360 | * 361 | * @return boolean true if and only if the server supports file upload. 362 | * 363 | * @deprecated Use getFileUploadSupport instead. 364 | * @see getFileUploadSupport() 365 | */ 366 | public function useFileUploadSupport() { 367 | return $this->getFileUploadSupport(); 368 | } 369 | 370 | /** 371 | * Sets the access token for api calls. Use this if you get 372 | * your access token by other means and just want the SDK 373 | * to use it. 374 | * 375 | * @param string $access_token an access token. 376 | * 377 | * @return BaseFacebook 378 | */ 379 | public function setAccessToken($access_token) { 380 | $this->accessToken = $access_token; 381 | return $this; 382 | } 383 | 384 | /** 385 | * Extend an access token, while removing the short-lived token that might 386 | * have been generated via client-side flow. Thanks to http://bit.ly/b0Pt0H 387 | * for the workaround. 388 | */ 389 | public function setExtendedAccessToken() { 390 | try { 391 | // need to circumvent json_decode by calling _oauthRequest 392 | // directly, since response isn't JSON format. 393 | $access_token_response = $this->_oauthRequest( 394 | $this->getUrl('graph', '/oauth/access_token'), 395 | $params = array( 396 | 'client_id' => $this->getAppId(), 397 | 'client_secret' => $this->getAppSecret(), 398 | 'grant_type' => 'fb_exchange_token', 399 | 'fb_exchange_token' => $this->getAccessToken(), 400 | ) 401 | ); 402 | } 403 | catch (FacebookApiException $e) { 404 | // most likely that user very recently revoked authorization. 405 | // In any event, we don't have an access token, so say so. 406 | return false; 407 | } 408 | 409 | if (empty($access_token_response)) { 410 | return false; 411 | } 412 | 413 | $response_params = array(); 414 | parse_str($access_token_response, $response_params); 415 | 416 | if (!isset($response_params['access_token'])) { 417 | return false; 418 | } 419 | 420 | $this->destroySession(); 421 | 422 | $this->setPersistentData( 423 | 'access_token', $response_params['access_token'] 424 | ); 425 | } 426 | 427 | /** 428 | * Determines the access token that should be used for API calls. 429 | * The first time this is called, $this->accessToken is set equal 430 | * to either a valid user access token, or it's set to the application 431 | * access token if a valid user access token wasn't available. Subsequent 432 | * calls return whatever the first call returned. 433 | * 434 | * @return string The access token 435 | */ 436 | public function getAccessToken() { 437 | if ($this->accessToken !== null) { 438 | // we've done this already and cached it. Just return. 439 | return $this->accessToken; 440 | } 441 | 442 | // first establish access token to be the application 443 | // access token, in case we navigate to the /oauth/access_token 444 | // endpoint, where SOME access token is required. 445 | $this->setAccessToken($this->getApplicationAccessToken()); 446 | $user_access_token = $this->getUserAccessToken(); 447 | if ($user_access_token) { 448 | $this->setAccessToken($user_access_token); 449 | } 450 | 451 | return $this->accessToken; 452 | } 453 | 454 | /** 455 | * Determines and returns the user access token, first using 456 | * the signed request if present, and then falling back on 457 | * the authorization code if present. The intent is to 458 | * return a valid user access token, or false if one is determined 459 | * to not be available. 460 | * 461 | * @return string A valid user access token, or false if one 462 | * could not be determined. 463 | */ 464 | protected function getUserAccessToken() { 465 | // first, consider a signed request if it's supplied. 466 | // if there is a signed request, then it alone determines 467 | // the access token. 468 | $signed_request = $this->getSignedRequest(); 469 | if ($signed_request) { 470 | // apps.facebook.com hands the access_token in the signed_request 471 | if (array_key_exists('oauth_token', $signed_request)) { 472 | $access_token = $signed_request['oauth_token']; 473 | $this->setPersistentData('access_token', $access_token); 474 | return $access_token; 475 | } 476 | 477 | // the JS SDK puts a code in with the redirect_uri of '' 478 | if (array_key_exists('code', $signed_request)) { 479 | $code = $signed_request['code']; 480 | if ($code && $code == $this->getPersistentData('code')) { 481 | // short-circuit if the code we have is the same as the one presented 482 | return $this->getPersistentData('access_token'); 483 | } 484 | 485 | $access_token = $this->getAccessTokenFromCode($code, ''); 486 | if ($access_token) { 487 | $this->setPersistentData('code', $code); 488 | $this->setPersistentData('access_token', $access_token); 489 | return $access_token; 490 | } 491 | } 492 | 493 | // signed request states there's no access token, so anything 494 | // stored should be cleared. 495 | $this->clearAllPersistentData(); 496 | return false; // respect the signed request's data, even 497 | // if there's an authorization code or something else 498 | } 499 | 500 | $code = $this->getCode(); 501 | if ($code && $code != $this->getPersistentData('code')) { 502 | $access_token = $this->getAccessTokenFromCode($code); 503 | if ($access_token) { 504 | $this->setPersistentData('code', $code); 505 | $this->setPersistentData('access_token', $access_token); 506 | return $access_token; 507 | } 508 | 509 | // code was bogus, so everything based on it should be invalidated. 510 | $this->clearAllPersistentData(); 511 | return false; 512 | } 513 | 514 | // as a fallback, just return whatever is in the persistent 515 | // store, knowing nothing explicit (signed request, authorization 516 | // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, 517 | // but it's the same as what's in the persistent store) 518 | return $this->getPersistentData('access_token'); 519 | } 520 | 521 | /** 522 | * Retrieve the signed request, either from a request parameter or, 523 | * if not present, from a cookie. 524 | * 525 | * @return string the signed request, if available, or null otherwise. 526 | */ 527 | public function getSignedRequest() { 528 | if (!$this->signedRequest) { 529 | if ($this->allowSignedRequest && !empty($_REQUEST['signed_request'])) { 530 | $this->signedRequest = $this->parseSignedRequest( 531 | $_REQUEST['signed_request'] 532 | ); 533 | } else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) { 534 | $this->signedRequest = $this->parseSignedRequest( 535 | $_COOKIE[$this->getSignedRequestCookieName()]); 536 | } 537 | } 538 | return $this->signedRequest; 539 | } 540 | 541 | /** 542 | * Get the UID of the connected user, or 0 543 | * if the Facebook user is not connected. 544 | * 545 | * @return string the UID if available. 546 | */ 547 | public function getUser() { 548 | if ($this->user !== null) { 549 | // we've already determined this and cached the value. 550 | return $this->user; 551 | } 552 | 553 | return $this->user = $this->getUserFromAvailableData(); 554 | } 555 | 556 | /** 557 | * Determines the connected user by first examining any signed 558 | * requests, then considering an authorization code, and then 559 | * falling back to any persistent store storing the user. 560 | * 561 | * @return integer The id of the connected Facebook user, 562 | * or 0 if no such user exists. 563 | */ 564 | protected function getUserFromAvailableData() { 565 | // if a signed request is supplied, then it solely determines 566 | // who the user is. 567 | $signed_request = $this->getSignedRequest(); 568 | if ($signed_request) { 569 | if (array_key_exists('user_id', $signed_request)) { 570 | $user = $signed_request['user_id']; 571 | 572 | if($user != $this->getPersistentData('user_id')){ 573 | $this->clearAllPersistentData(); 574 | } 575 | 576 | $this->setPersistentData('user_id', $signed_request['user_id']); 577 | return $user; 578 | } 579 | 580 | // if the signed request didn't present a user id, then invalidate 581 | // all entries in any persistent store. 582 | $this->clearAllPersistentData(); 583 | return 0; 584 | } 585 | 586 | $user = $this->getPersistentData('user_id', $default = 0); 587 | $persisted_access_token = $this->getPersistentData('access_token'); 588 | 589 | // use access_token to fetch user id if we have a user access_token, or if 590 | // the cached access token has changed. 591 | $access_token = $this->getAccessToken(); 592 | if ($access_token && 593 | $access_token != $this->getApplicationAccessToken() && 594 | !($user && $persisted_access_token == $access_token)) { 595 | $user = $this->getUserFromAccessToken(); 596 | if ($user) { 597 | $this->setPersistentData('user_id', $user); 598 | } else { 599 | $this->clearAllPersistentData(); 600 | } 601 | } 602 | 603 | return $user; 604 | } 605 | 606 | /** 607 | * Get a Login URL for use with redirects. By default, full page redirect is 608 | * assumed. If you are using the generated URL with a window.open() call in 609 | * JavaScript, you can pass in display=popup as part of the $params. 610 | * 611 | * The parameters: 612 | * - redirect_uri: the url to go to after a successful login 613 | * - scope: comma separated list of requested extended perms 614 | * 615 | * @param array $params Provide custom parameters 616 | * @return string The URL for the login flow 617 | */ 618 | public function getLoginUrl($params=array()) { 619 | $this->establishCSRFTokenState(); 620 | $currentUrl = $this->getCurrentUrl(); 621 | 622 | // if 'scope' is passed as an array, convert to comma separated list 623 | $scopeParams = isset($params['scope']) ? $params['scope'] : null; 624 | if ($scopeParams && is_array($scopeParams)) { 625 | $params['scope'] = implode(',', $scopeParams); 626 | } 627 | 628 | return $this->getUrl( 629 | 'www', 630 | 'dialog/oauth', 631 | array_merge( 632 | array( 633 | 'client_id' => $this->getAppId(), 634 | 'redirect_uri' => $currentUrl, // possibly overwritten 635 | 'state' => $this->state, 636 | 'sdk' => 'php-sdk-'.self::VERSION 637 | ), 638 | $params 639 | )); 640 | } 641 | 642 | /** 643 | * Get a Logout URL suitable for use with redirects. 644 | * 645 | * The parameters: 646 | * - next: the url to go to after a successful logout 647 | * 648 | * @param array $params Provide custom parameters 649 | * @return string The URL for the logout flow 650 | */ 651 | public function getLogoutUrl($params=array()) { 652 | return $this->getUrl( 653 | 'www', 654 | 'logout.php', 655 | array_merge(array( 656 | 'next' => $this->getCurrentUrl(), 657 | 'access_token' => $this->getUserAccessToken(), 658 | ), $params) 659 | ); 660 | } 661 | 662 | /** 663 | * Get a login status URL to fetch the status from Facebook. 664 | * 665 | * @param array $params Provide custom parameters 666 | * @return string The URL for the logout flow 667 | */ 668 | public function getLoginStatusUrl($params=array()) { 669 | return $this->getLoginUrl( 670 | array_merge(array( 671 | 'response_type' => 'code', 672 | 'display' => 'none', 673 | ), $params) 674 | ); 675 | } 676 | 677 | /** 678 | * Make an API call. 679 | * 680 | * @return mixed The decoded response 681 | */ 682 | public function api(/* polymorphic */) { 683 | $args = func_get_args(); 684 | if (is_array($args[0])) { 685 | return $this->_restserver($args[0]); 686 | } else { 687 | return call_user_func_array(array($this, '_graph'), $args); 688 | } 689 | } 690 | 691 | /** 692 | * Constructs and returns the name of the cookie that 693 | * potentially houses the signed request for the app user. 694 | * The cookie is not set by the BaseFacebook class, but 695 | * it may be set by the JavaScript SDK. 696 | * 697 | * @return string the name of the cookie that would house 698 | * the signed request value. 699 | */ 700 | protected function getSignedRequestCookieName() { 701 | return 'fbsr_'.$this->getAppId(); 702 | } 703 | 704 | /** 705 | * Constructs and returns the name of the cookie that potentially contain 706 | * metadata. The cookie is not set by the BaseFacebook class, but it may be 707 | * set by the JavaScript SDK. 708 | * 709 | * @return string the name of the cookie that would house metadata. 710 | */ 711 | protected function getMetadataCookieName() { 712 | return 'fbm_'.$this->getAppId(); 713 | } 714 | 715 | /** 716 | * Get the authorization code from the query parameters, if it exists, 717 | * and otherwise return false to signal no authorization code was 718 | * discoverable. 719 | * 720 | * @return mixed The authorization code, or false if the authorization 721 | * code could not be determined. 722 | */ 723 | protected function getCode() { 724 | if (!isset($_REQUEST['code']) || !isset($_REQUEST['state'])) { 725 | return false; 726 | } 727 | if ($this->state === $_REQUEST['state']) { 728 | // CSRF state has done its job, so clear it 729 | $this->state = null; 730 | $this->clearPersistentData('state'); 731 | return $_REQUEST['code']; 732 | } 733 | self::errorLog('CSRF state token does not match one provided.'); 734 | 735 | return false; 736 | } 737 | 738 | /** 739 | * Retrieves the UID with the understanding that 740 | * $this->accessToken has already been set and is 741 | * seemingly legitimate. It relies on Facebook's Graph API 742 | * to retrieve user information and then extract 743 | * the user ID. 744 | * 745 | * @return integer Returns the UID of the Facebook user, or 0 746 | * if the Facebook user could not be determined. 747 | */ 748 | protected function getUserFromAccessToken() { 749 | try { 750 | $user_info = $this->api('/me'); 751 | return $user_info['id']; 752 | } catch (FacebookApiException $e) { 753 | return 0; 754 | } 755 | } 756 | 757 | /** 758 | * Returns the access token that should be used for logged out 759 | * users when no authorization code is available. 760 | * 761 | * @return string The application access token, useful for gathering 762 | * public information about users and applications. 763 | */ 764 | public function getApplicationAccessToken() { 765 | return $this->appId.'|'.$this->appSecret; 766 | } 767 | 768 | /** 769 | * Lays down a CSRF state token for this process. 770 | * 771 | * @return void 772 | */ 773 | protected function establishCSRFTokenState() { 774 | if ($this->state === null) { 775 | $this->state = md5(uniqid(mt_rand(), true)); 776 | $this->setPersistentData('state', $this->state); 777 | } 778 | } 779 | 780 | /** 781 | * Retrieves an access token for the given authorization code 782 | * (previously generated from www.facebook.com on behalf of 783 | * a specific user). The authorization code is sent to graph.facebook.com 784 | * and a legitimate access token is generated provided the access token 785 | * and the user for which it was generated all match, and the user is 786 | * either logged in to Facebook or has granted an offline access permission. 787 | * 788 | * @param string $code An authorization code. 789 | * @param string $redirect_uri Optional redirect URI. Default null 790 | * 791 | * @return mixed An access token exchanged for the authorization code, or 792 | * false if an access token could not be generated. 793 | */ 794 | protected function getAccessTokenFromCode($code, $redirect_uri = null) { 795 | if (empty($code)) { 796 | return false; 797 | } 798 | 799 | if ($redirect_uri === null) { 800 | $redirect_uri = $this->getCurrentUrl(); 801 | } 802 | 803 | try { 804 | // need to circumvent json_decode by calling _oauthRequest 805 | // directly, since response isn't JSON format. 806 | $access_token_response = 807 | $this->_oauthRequest( 808 | $this->getUrl('graph', '/oauth/access_token'), 809 | $params = array('client_id' => $this->getAppId(), 810 | 'client_secret' => $this->getAppSecret(), 811 | 'redirect_uri' => $redirect_uri, 812 | 'code' => $code)); 813 | } catch (FacebookApiException $e) { 814 | // most likely that user very recently revoked authorization. 815 | // In any event, we don't have an access token, so say so. 816 | return false; 817 | } 818 | 819 | if (empty($access_token_response)) { 820 | return false; 821 | } 822 | 823 | $response_params = array(); 824 | parse_str($access_token_response, $response_params); 825 | if (!isset($response_params['access_token'])) { 826 | return false; 827 | } 828 | 829 | return $response_params['access_token']; 830 | } 831 | 832 | /** 833 | * Invoke the old restserver.php endpoint. 834 | * 835 | * @param array $params Method call object 836 | * 837 | * @return mixed The decoded response object 838 | * @throws FacebookApiException 839 | */ 840 | protected function _restserver($params) { 841 | // generic application level parameters 842 | $params['api_key'] = $this->getAppId(); 843 | $params['format'] = 'json-strings'; 844 | 845 | $result = json_decode($this->_oauthRequest( 846 | $this->getApiUrl($params['method']), 847 | $params 848 | ), true); 849 | 850 | // results are returned, errors are thrown 851 | if (is_array($result) && isset($result['error_code'])) { 852 | $this->throwAPIException($result); 853 | // @codeCoverageIgnoreStart 854 | } 855 | // @codeCoverageIgnoreEnd 856 | 857 | $method = strtolower($params['method']); 858 | if ($method === 'auth.expiresession' || 859 | $method === 'auth.revokeauthorization') { 860 | $this->destroySession(); 861 | } 862 | 863 | return $result; 864 | } 865 | 866 | /** 867 | * Return true if this is video post. 868 | * 869 | * @param string $path The path 870 | * @param string $method The http method (default 'GET') 871 | * 872 | * @return boolean true if this is video post 873 | */ 874 | protected function isVideoPost($path, $method = 'GET') { 875 | if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) { 876 | return true; 877 | } 878 | return false; 879 | } 880 | 881 | /** 882 | * Invoke the Graph API. 883 | * 884 | * @param string $path The path (required) 885 | * @param string $method The http method (default 'GET') 886 | * @param array $params The query/post data 887 | * 888 | * @return mixed The decoded response object 889 | * @throws FacebookApiException 890 | */ 891 | protected function _graph($path, $method = 'GET', $params = array()) { 892 | if (is_array($method) && empty($params)) { 893 | $params = $method; 894 | $method = 'GET'; 895 | } 896 | $params['method'] = $method; // method override as we always do a POST 897 | 898 | if ($this->isVideoPost($path, $method)) { 899 | $domainKey = 'graph_video'; 900 | } else { 901 | $domainKey = 'graph'; 902 | } 903 | 904 | $result = json_decode($this->_oauthRequest( 905 | $this->getUrl($domainKey, $path), 906 | $params 907 | ), true); 908 | 909 | // results are returned, errors are thrown 910 | if (is_array($result) && isset($result['error'])) { 911 | $this->throwAPIException($result); 912 | // @codeCoverageIgnoreStart 913 | } 914 | // @codeCoverageIgnoreEnd 915 | 916 | return $result; 917 | } 918 | 919 | /** 920 | * Make a OAuth Request. 921 | * 922 | * @param string $url The path (required) 923 | * @param array $params The query/post data 924 | * 925 | * @return string The decoded response object 926 | * @throws FacebookApiException 927 | */ 928 | protected function _oauthRequest($url, $params) { 929 | if (!isset($params['access_token'])) { 930 | $params['access_token'] = $this->getAccessToken(); 931 | } 932 | 933 | if (isset($params['access_token']) && !isset($params['appsecret_proof'])) { 934 | $params['appsecret_proof'] = $this->getAppSecretProof($params['access_token']); 935 | } 936 | 937 | // json_encode all params values that are not strings 938 | foreach ($params as $key => $value) { 939 | if (!is_string($value) && !($value instanceof CURLFile)) { 940 | $params[$key] = json_encode($value); 941 | } 942 | } 943 | 944 | return $this->makeRequest($url, $params); 945 | } 946 | 947 | /** 948 | * Generate a proof of App Secret 949 | * This is required for all API calls originating from a server 950 | * It is a sha256 hash of the access_token made using the app secret 951 | * 952 | * @param string $access_token The access_token to be hashed (required) 953 | * 954 | * @return string The sha256 hash of the access_token 955 | */ 956 | protected function getAppSecretProof($access_token) { 957 | return hash_hmac('sha256', $access_token, $this->getAppSecret()); 958 | } 959 | 960 | /** 961 | * Makes an HTTP request. This method can be overridden by subclasses if 962 | * developers want to do fancier things or use something other than curl to 963 | * make the request. 964 | * 965 | * @param string $url The URL to make the request to 966 | * @param array $params The parameters to use for the POST body 967 | * @param CurlHandler $ch Initialized curl handle 968 | * 969 | * @return string The response text 970 | */ 971 | protected function makeRequest($url, $params, $ch=null) { 972 | if (!$ch) { 973 | $ch = curl_init(); 974 | } 975 | 976 | $opts = self::$CURL_OPTS; 977 | if ($this->getFileUploadSupport()) { 978 | $opts[CURLOPT_POSTFIELDS] = $params; 979 | } else { 980 | $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); 981 | } 982 | $opts[CURLOPT_URL] = $url; 983 | 984 | // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait 985 | // for 2 seconds if the server does not support this header. 986 | if (isset($opts[CURLOPT_HTTPHEADER])) { 987 | $existing_headers = $opts[CURLOPT_HTTPHEADER]; 988 | $existing_headers[] = 'Expect:'; 989 | $opts[CURLOPT_HTTPHEADER] = $existing_headers; 990 | } else { 991 | $opts[CURLOPT_HTTPHEADER] = array('Expect:'); 992 | } 993 | 994 | curl_setopt_array($ch, $opts); 995 | $result = curl_exec($ch); 996 | 997 | $errno = curl_errno($ch); 998 | // CURLE_SSL_CACERT || CURLE_SSL_CACERT_BADFILE 999 | if ($errno == 60 || $errno == 77) { 1000 | self::errorLog('Invalid or no certificate authority found, '. 1001 | 'using bundled information'); 1002 | curl_setopt($ch, CURLOPT_CAINFO, 1003 | dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt'); 1004 | $result = curl_exec($ch); 1005 | } 1006 | 1007 | // With dual stacked DNS responses, it's possible for a server to 1008 | // have IPv6 enabled but not have IPv6 connectivity. If this is 1009 | // the case, curl will try IPv4 first and if that fails, then it will 1010 | // fall back to IPv6 and the error EHOSTUNREACH is returned by the 1011 | // operating system. 1012 | if ($result === false && empty($opts[CURLOPT_IPRESOLVE])) { 1013 | $matches = array(); 1014 | $regex = '/Failed to connect to ([^:].*): Network is unreachable/'; 1015 | if (preg_match($regex, curl_error($ch), $matches)) { 1016 | if (strlen(@inet_pton($matches[1])) === 16) { 1017 | self::errorLog('Invalid IPv6 configuration on server, '. 1018 | 'Please disable or get native IPv6 on your server.'); 1019 | self::$CURL_OPTS[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; 1020 | curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 1021 | $result = curl_exec($ch); 1022 | } 1023 | } 1024 | } 1025 | 1026 | if ($result === false) { 1027 | $e = new FacebookApiException(array( 1028 | 'error_code' => curl_errno($ch), 1029 | 'error' => array( 1030 | 'message' => curl_error($ch), 1031 | 'type' => 'CurlException', 1032 | ), 1033 | )); 1034 | curl_close($ch); 1035 | throw $e; 1036 | } 1037 | curl_close($ch); 1038 | return $result; 1039 | } 1040 | 1041 | /** 1042 | * Parses a signed_request and validates the signature. 1043 | * 1044 | * @param string $signed_request A signed token 1045 | * 1046 | * @return array The payload inside it or null if the sig is wrong 1047 | */ 1048 | protected function parseSignedRequest($signed_request) { 1049 | 1050 | if (!$signed_request || strpos($signed_request, '.') === false) { 1051 | self::errorLog('Signed request was invalid!'); 1052 | return null; 1053 | } 1054 | 1055 | list($encoded_sig, $payload) = explode('.', $signed_request, 2); 1056 | 1057 | // decode the data 1058 | $sig = self::base64UrlDecode($encoded_sig); 1059 | $data = json_decode(self::base64UrlDecode($payload), true); 1060 | 1061 | if (!isset($data['algorithm']) 1062 | || strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM 1063 | ) { 1064 | self::errorLog( 1065 | 'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM); 1066 | return null; 1067 | } 1068 | 1069 | // check sig 1070 | $expected_sig = hash_hmac('sha256', $payload, 1071 | $this->getAppSecret(), $raw = true); 1072 | 1073 | if (strlen($expected_sig) !== strlen($sig)) { 1074 | self::errorLog('Bad Signed JSON signature!'); 1075 | return null; 1076 | } 1077 | 1078 | $result = 0; 1079 | for ($i = 0; $i < strlen($expected_sig); $i++) { 1080 | $result |= ord($expected_sig[$i]) ^ ord($sig[$i]); 1081 | } 1082 | 1083 | if ($result == 0) { 1084 | return $data; 1085 | } else { 1086 | self::errorLog('Bad Signed JSON signature!'); 1087 | return null; 1088 | } 1089 | } 1090 | 1091 | /** 1092 | * Makes a signed_request blob using the given data. 1093 | * 1094 | * @param array $data The data array. 1095 | * 1096 | * @return string The signed request. 1097 | */ 1098 | protected function makeSignedRequest($data) { 1099 | if (!is_array($data)) { 1100 | throw new InvalidArgumentException( 1101 | 'makeSignedRequest expects an array. Got: ' . print_r($data, true)); 1102 | } 1103 | $data['algorithm'] = self::SIGNED_REQUEST_ALGORITHM; 1104 | $data['issued_at'] = time(); 1105 | $json = json_encode($data); 1106 | $b64 = self::base64UrlEncode($json); 1107 | $raw_sig = hash_hmac('sha256', $b64, $this->getAppSecret(), $raw = true); 1108 | $sig = self::base64UrlEncode($raw_sig); 1109 | return $sig.'.'.$b64; 1110 | } 1111 | 1112 | /** 1113 | * Build the URL for api given parameters. 1114 | * 1115 | * @param string $method The method name. 1116 | * 1117 | * @return string The URL for the given parameters 1118 | */ 1119 | protected function getApiUrl($method) { 1120 | static $READ_ONLY_CALLS = 1121 | array('admin.getallocation' => 1, 1122 | 'admin.getappproperties' => 1, 1123 | 'admin.getbannedusers' => 1, 1124 | 'admin.getlivestreamvialink' => 1, 1125 | 'admin.getmetrics' => 1, 1126 | 'admin.getrestrictioninfo' => 1, 1127 | 'application.getpublicinfo' => 1, 1128 | 'auth.getapppublickey' => 1, 1129 | 'auth.getsession' => 1, 1130 | 'auth.getsignedpublicsessiondata' => 1, 1131 | 'comments.get' => 1, 1132 | 'connect.getunconnectedfriendscount' => 1, 1133 | 'dashboard.getactivity' => 1, 1134 | 'dashboard.getcount' => 1, 1135 | 'dashboard.getglobalnews' => 1, 1136 | 'dashboard.getnews' => 1, 1137 | 'dashboard.multigetcount' => 1, 1138 | 'dashboard.multigetnews' => 1, 1139 | 'data.getcookies' => 1, 1140 | 'events.get' => 1, 1141 | 'events.getmembers' => 1, 1142 | 'fbml.getcustomtags' => 1, 1143 | 'feed.getappfriendstories' => 1, 1144 | 'feed.getregisteredtemplatebundlebyid' => 1, 1145 | 'feed.getregisteredtemplatebundles' => 1, 1146 | 'fql.multiquery' => 1, 1147 | 'fql.query' => 1, 1148 | 'friends.arefriends' => 1, 1149 | 'friends.get' => 1, 1150 | 'friends.getappusers' => 1, 1151 | 'friends.getlists' => 1, 1152 | 'friends.getmutualfriends' => 1, 1153 | 'gifts.get' => 1, 1154 | 'groups.get' => 1, 1155 | 'groups.getmembers' => 1, 1156 | 'intl.gettranslations' => 1, 1157 | 'links.get' => 1, 1158 | 'notes.get' => 1, 1159 | 'notifications.get' => 1, 1160 | 'pages.getinfo' => 1, 1161 | 'pages.isadmin' => 1, 1162 | 'pages.isappadded' => 1, 1163 | 'pages.isfan' => 1, 1164 | 'permissions.checkavailableapiaccess' => 1, 1165 | 'permissions.checkgrantedapiaccess' => 1, 1166 | 'photos.get' => 1, 1167 | 'photos.getalbums' => 1, 1168 | 'photos.gettags' => 1, 1169 | 'profile.getinfo' => 1, 1170 | 'profile.getinfooptions' => 1, 1171 | 'stream.get' => 1, 1172 | 'stream.getcomments' => 1, 1173 | 'stream.getfilters' => 1, 1174 | 'users.getinfo' => 1, 1175 | 'users.getloggedinuser' => 1, 1176 | 'users.getstandardinfo' => 1, 1177 | 'users.hasapppermission' => 1, 1178 | 'users.isappuser' => 1, 1179 | 'users.isverified' => 1, 1180 | 'video.getuploadlimits' => 1); 1181 | $name = 'api'; 1182 | if (isset($READ_ONLY_CALLS[strtolower($method)])) { 1183 | $name = 'api_read'; 1184 | } else if (strtolower($method) == 'video.upload') { 1185 | $name = 'api_video'; 1186 | } 1187 | return self::getUrl($name, 'restserver.php'); 1188 | } 1189 | 1190 | /** 1191 | * Build the URL for given domain alias, path and parameters. 1192 | * 1193 | * @param string $name The name of the domain 1194 | * @param string $path Optional path (without a leading slash) 1195 | * @param array $params Optional query parameters 1196 | * 1197 | * @return string The URL for the given parameters 1198 | */ 1199 | protected function getUrl($name, $path='', $params=array()) { 1200 | $url = self::$DOMAIN_MAP[$name]; 1201 | if ($path) { 1202 | if ($path[0] === '/') { 1203 | $path = substr($path, 1); 1204 | } 1205 | $url .= $path; 1206 | } 1207 | if ($params) { 1208 | $url .= '?' . http_build_query($params, null, '&'); 1209 | } 1210 | 1211 | return $url; 1212 | } 1213 | 1214 | /** 1215 | * Returns the HTTP Host 1216 | * 1217 | * @return string The HTTP Host 1218 | */ 1219 | protected function getHttpHost() { 1220 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { 1221 | $forwardProxies = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']); 1222 | if (!empty($forwardProxies)) { 1223 | return $forwardProxies[0]; 1224 | } 1225 | } 1226 | return $_SERVER['HTTP_HOST']; 1227 | } 1228 | 1229 | /** 1230 | * Returns the HTTP Protocol 1231 | * 1232 | * @return string The HTTP Protocol 1233 | */ 1234 | protected function getHttpProtocol() { 1235 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 1236 | if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { 1237 | return 'https'; 1238 | } 1239 | return 'http'; 1240 | } 1241 | /*apache + variants specific way of checking for https*/ 1242 | if (isset($_SERVER['HTTPS']) && 1243 | ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) { 1244 | return 'https'; 1245 | } 1246 | /*nginx way of checking for https*/ 1247 | if (isset($_SERVER['SERVER_PORT']) && 1248 | ($_SERVER['SERVER_PORT'] === '443')) { 1249 | return 'https'; 1250 | } 1251 | return 'http'; 1252 | } 1253 | 1254 | /** 1255 | * Returns the base domain used for the cookie. 1256 | * 1257 | * @return string The base domain 1258 | */ 1259 | protected function getBaseDomain() { 1260 | // The base domain is stored in the metadata cookie if not we fallback 1261 | // to the current hostname 1262 | $metadata = $this->getMetadataCookie(); 1263 | if (array_key_exists('base_domain', $metadata) && 1264 | !empty($metadata['base_domain'])) { 1265 | return trim($metadata['base_domain'], '.'); 1266 | } 1267 | return $this->getHttpHost(); 1268 | } 1269 | 1270 | /** 1271 | * Returns the Current URL, stripping it of known FB parameters that should 1272 | * not persist. 1273 | * 1274 | * @return string The current URL 1275 | */ 1276 | protected function getCurrentUrl() { 1277 | $protocol = $this->getHttpProtocol() . '://'; 1278 | $host = $this->getHttpHost(); 1279 | $currentUrl = $protocol.$host.$_SERVER['REQUEST_URI']; 1280 | $parts = parse_url($currentUrl); 1281 | 1282 | $query = ''; 1283 | if (!empty($parts['query'])) { 1284 | // drop known fb params 1285 | $params = explode('&', $parts['query']); 1286 | $retained_params = array(); 1287 | foreach ($params as $param) { 1288 | if ($this->shouldRetainParam($param)) { 1289 | $retained_params[] = $param; 1290 | } 1291 | } 1292 | 1293 | if (!empty($retained_params)) { 1294 | $query = '?'.implode($retained_params, '&'); 1295 | } 1296 | } 1297 | 1298 | // use port if non default 1299 | $port = 1300 | isset($parts['port']) && 1301 | (($protocol === 'http://' && $parts['port'] !== 80) || 1302 | ($protocol === 'https://' && $parts['port'] !== 443)) 1303 | ? ':' . $parts['port'] : ''; 1304 | 1305 | // rebuild 1306 | return $protocol . $parts['host'] . $port . $parts['path'] . $query; 1307 | } 1308 | 1309 | /** 1310 | * Returns true if and only if the key or key/value pair should 1311 | * be retained as part of the query string. This amounts to 1312 | * a brute-force search of the very small list of Facebook-specific 1313 | * params that should be stripped out. 1314 | * 1315 | * @param string $param A key or key/value pair within a URL's query (e.g. 1316 | * 'foo=a', 'foo=', or 'foo'. 1317 | * 1318 | * @return boolean 1319 | */ 1320 | protected function shouldRetainParam($param) { 1321 | foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { 1322 | if ($param === $drop_query_param || 1323 | strpos($param, $drop_query_param.'=') === 0) { 1324 | return false; 1325 | } 1326 | } 1327 | 1328 | return true; 1329 | } 1330 | 1331 | /** 1332 | * Analyzes the supplied result to see if it was thrown 1333 | * because the access token is no longer valid. If that is 1334 | * the case, then we destroy the session. 1335 | * 1336 | * @param array $result A record storing the error message returned 1337 | * by a failed API call. 1338 | */ 1339 | protected function throwAPIException($result) { 1340 | $e = new FacebookApiException($result); 1341 | switch ($e->getType()) { 1342 | // OAuth 2.0 Draft 00 style 1343 | case 'OAuthException': 1344 | // OAuth 2.0 Draft 10 style 1345 | case 'invalid_token': 1346 | // REST server errors are just Exceptions 1347 | case 'Exception': 1348 | $message = $e->getMessage(); 1349 | if ((strpos($message, 'Error validating access token') !== false) || 1350 | (strpos($message, 'Invalid OAuth access token') !== false) || 1351 | (strpos($message, 'An active access token must be used') !== false) 1352 | ) { 1353 | $this->destroySession(); 1354 | } 1355 | break; 1356 | } 1357 | 1358 | throw $e; 1359 | } 1360 | 1361 | 1362 | /** 1363 | * Prints to the error log if you aren't in command line mode. 1364 | * 1365 | * @param string $msg Log message 1366 | */ 1367 | protected static function errorLog($msg) { 1368 | // disable error log if we are running in a CLI environment 1369 | // @codeCoverageIgnoreStart 1370 | if (php_sapi_name() != 'cli') { 1371 | error_log($msg); 1372 | } 1373 | // uncomment this if you want to see the errors on the page 1374 | // print 'error_log: '.$msg."\n"; 1375 | // @codeCoverageIgnoreEnd 1376 | } 1377 | 1378 | /** 1379 | * Base64 encoding that doesn't need to be urlencode()ed. 1380 | * Exactly the same as base64_encode except it uses 1381 | * - instead of + 1382 | * _ instead of / 1383 | * No padded = 1384 | * 1385 | * @param string $input base64UrlEncoded input 1386 | * 1387 | * @return string The decoded string 1388 | */ 1389 | protected static function base64UrlDecode($input) { 1390 | return base64_decode(strtr($input, '-_', '+/')); 1391 | } 1392 | 1393 | /** 1394 | * Base64 encoding that doesn't need to be urlencode()ed. 1395 | * Exactly the same as base64_encode except it uses 1396 | * - instead of + 1397 | * _ instead of / 1398 | * 1399 | * @param string $input The input to encode 1400 | * @return string The base64Url encoded input, as a string. 1401 | */ 1402 | protected static function base64UrlEncode($input) { 1403 | $str = strtr(base64_encode($input), '+/', '-_'); 1404 | $str = str_replace('=', '', $str); 1405 | return $str; 1406 | } 1407 | 1408 | /** 1409 | * Destroy the current session 1410 | */ 1411 | public function destroySession() { 1412 | $this->accessToken = null; 1413 | $this->signedRequest = null; 1414 | $this->user = null; 1415 | $this->clearAllPersistentData(); 1416 | 1417 | // Javascript sets a cookie that will be used in getSignedRequest that we 1418 | // need to clear if we can 1419 | $cookie_name = $this->getSignedRequestCookieName(); 1420 | if (array_key_exists($cookie_name, $_COOKIE)) { 1421 | unset($_COOKIE[$cookie_name]); 1422 | if (!headers_sent()) { 1423 | $base_domain = $this->getBaseDomain(); 1424 | setcookie($cookie_name, '', 1, '/', '.'.$base_domain); 1425 | } else { 1426 | // @codeCoverageIgnoreStart 1427 | self::errorLog( 1428 | 'There exists a cookie that we wanted to clear that we couldn\'t '. 1429 | 'clear because headers was already sent. Make sure to do the first '. 1430 | 'API call before outputing anything.' 1431 | ); 1432 | // @codeCoverageIgnoreEnd 1433 | } 1434 | } 1435 | } 1436 | 1437 | /** 1438 | * Parses the metadata cookie that our Javascript API set 1439 | * 1440 | * @return array an array mapping key to value 1441 | */ 1442 | protected function getMetadataCookie() { 1443 | $cookie_name = $this->getMetadataCookieName(); 1444 | if (!array_key_exists($cookie_name, $_COOKIE)) { 1445 | return array(); 1446 | } 1447 | 1448 | // The cookie value can be wrapped in "-characters so remove them 1449 | $cookie_value = trim($_COOKIE[$cookie_name], '"'); 1450 | 1451 | if (empty($cookie_value)) { 1452 | return array(); 1453 | } 1454 | 1455 | $parts = explode('&', $cookie_value); 1456 | $metadata = array(); 1457 | foreach ($parts as $part) { 1458 | $pair = explode('=', $part, 2); 1459 | if (!empty($pair[0])) { 1460 | $metadata[urldecode($pair[0])] = 1461 | (count($pair) > 1) ? urldecode($pair[1]) : ''; 1462 | } 1463 | } 1464 | 1465 | return $metadata; 1466 | } 1467 | 1468 | /** 1469 | * Finds whether the given domain is allowed or not 1470 | * 1471 | * @param string $big The value to be checked against $small 1472 | * @param string $small The input string 1473 | * 1474 | * @return boolean Returns TRUE if $big matches $small 1475 | */ 1476 | protected static function isAllowedDomain($big, $small) { 1477 | if ($big === $small) { 1478 | return true; 1479 | } 1480 | return self::endsWith($big, '.'.$small); 1481 | } 1482 | 1483 | /** 1484 | * Checks if $big string ends with $small string 1485 | * 1486 | * @param string $big The value to be checked against $small 1487 | * @param string $small The input string 1488 | * 1489 | * @return boolean TRUE if $big ends with $small 1490 | */ 1491 | protected static function endsWith($big, $small) { 1492 | $len = strlen($small); 1493 | if ($len === 0) { 1494 | return true; 1495 | } 1496 | return substr($big, -$len) === $small; 1497 | } 1498 | 1499 | /** 1500 | * Each of the following four methods should be overridden in 1501 | * a concrete subclass, as they are in the provided Facebook class. 1502 | * The Facebook class uses PHP sessions to provide a primitive 1503 | * persistent store, but another subclass--one that you implement-- 1504 | * might use a database, memcache, or an in-memory cache. 1505 | * 1506 | * @see Facebook 1507 | */ 1508 | 1509 | /** 1510 | * Stores the given ($key, $value) pair, so that future calls to 1511 | * getPersistentData($key) return $value. This call may be in another request. 1512 | * 1513 | * @param string $key 1514 | * @param array $value 1515 | * 1516 | * @return void 1517 | */ 1518 | abstract protected function setPersistentData($key, $value); 1519 | 1520 | /** 1521 | * Get the data for $key, persisted by BaseFacebook::setPersistentData() 1522 | * 1523 | * @param string $key The key of the data to retrieve 1524 | * @param boolean $default The default value to return if $key is not found 1525 | * 1526 | * @return mixed 1527 | */ 1528 | abstract protected function getPersistentData($key, $default = false); 1529 | 1530 | /** 1531 | * Clear the data with $key from the persistent storage 1532 | * 1533 | * @param string $key 1534 | * 1535 | * @return void 1536 | */ 1537 | abstract protected function clearPersistentData($key); 1538 | 1539 | /** 1540 | * Clear all data from the persistent storage 1541 | * 1542 | * @return void 1543 | */ 1544 | abstract protected function clearAllPersistentData(); 1545 | } 1546 | --------------------------------------------------------------------------------