Compare commits
	
		
			114 Commits
		
	
	
		
			apfeed
			...
			actor_outb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5911b077ca | ||
|  | 789688d60a | ||
|  | 6fbf37b7fe | ||
|  | b2f6f9f08e | ||
|  | 935495415b | ||
|  | 334923e542 | ||
|  | 76bea7b17e | ||
|  | 0475954741 | ||
|  | ca8867c1c0 | ||
|  | fcbdf6aef2 | ||
|  | f3ebbf472b | ||
|  | 287ebeb521 | ||
|  | edc62d4cb0 | ||
|  | b9c76c2901 | ||
|  | 47b1f2219c | ||
|  | c10585da3f | ||
|  | 73d5fa79d7 | ||
|  | 88e69813ac | ||
|  | ee9cf5ef9f | ||
|  | 0c953142c4 | ||
|  | 2bada457c2 | ||
|  | dca1d8f0c5 | ||
|  | 7cf223fb24 | ||
|  | 6a238e4176 | ||
|  | 43ebdfa727 | ||
|  | de432cda88 | ||
|  | 442e66d112 | ||
|  | f25d8278b1 | ||
|  | 1f77983891 | ||
|  | 2b98344bc4 | ||
|  | aa491271f7 | ||
|  | cfc4eece38 | ||
|  | 7ce112471d | ||
|  | 6a608f08ed | ||
|  | f38b6c482f | ||
|  | aa49e97075 | ||
|  | 554ba6dc91 | ||
|  | 5226ca7d81 | ||
|  | 0384890f7b | ||
|  | 5f979a32f9 | ||
|  | 0965ae459f | ||
|  | e3086351e0 | ||
|  | 956d5c9b7d | ||
|  | 64dbb7b539 | ||
|  | c8f3f079cc | ||
|  | 8e80fb3528 | ||
|  | 5c9eac7d97 | ||
|  | 0ae5b603ee | ||
|  | 1b1631b530 | ||
|  | a704053f6d | ||
|  | 1d0d08544a | ||
|  | 51ebdcae30 | ||
|  | 9f61de3c87 | ||
|  | 12e5dc4b59 | ||
|  | 8b860d6da2 | ||
|  | 1f04b7ed40 | ||
|  | 133066b9ac | ||
|  | 6e55a72179 | ||
|  | db3ce1e34a | ||
|  | ec49aee44b | ||
|  | 30affa954f | ||
|  | d76ac3760b | ||
|  | 69b252b244 | ||
|  | 42bfb78184 | ||
|  | 6943eee623 | ||
|  | 5c630c39a0 | ||
|  | aac82c7b2a | ||
|  | 83f5c513b4 | ||
|  | 5196edafa0 | ||
|  | be1aadcd4c | ||
|  | e4a65130f8 | ||
|  | 8f6c120620 | ||
|  | a17e83582c | ||
|  | 372a65b423 | ||
|  | f90b3773d1 | ||
|  | 9c6aff46d9 | ||
|  | b4880713d5 | ||
|  | edfd52e35f | ||
|  | 80265024a7 | ||
|  | 3c021db5f0 | ||
|  | 9fddc0f606 | ||
|  | dd5af79304 | ||
|  | c7a758b6c8 | ||
|  | bb4bcd8ea1 | ||
|  | 12b1fd7b1f | ||
|  | 5c26e34f5a | ||
|  | aa196d383a | ||
|  | b7ac51a967 | ||
|  | 933a228072 | ||
|  | b9a74d6ba4 | ||
|  | dd01971997 | ||
|  | 3121d3435d | ||
|  | d90fabd15e | ||
|  | f77bad0159 | ||
|  | f8048c7565 | ||
|  | 20738f48cd | ||
|  | edb3633bcd | ||
|  | 5c351efb06 | ||
|  | 58262451e3 | ||
|  | 995aec80c7 | ||
|  | 5ee0fcbd9b | ||
|  | 4edd3ef398 | ||
|  | 01c16fcef0 | ||
|  | e0d5b2ebd7 | ||
|  | bff507bab8 | ||
|  | 78113620eb | ||
|  | eaad9423dd | ||
|  | e377b87ff7 | ||
|  | b359c8206d | ||
|  | 30c073538c | ||
|  | 2d0f3de52a | ||
|  | b693dab832 | ||
|  | a5b56b3089 | ||
|  | 2d4e634fad | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | /vendor/ | ||||||
|  | composer.lock | ||||||
							
								
								
									
										1046
									
								
								ActivityPubPlugin.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										1046
									
								
								ActivityPubPlugin.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										93
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										93
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | # Contributing | ||||||
|  |  | ||||||
|  | When contributing to this repository, please first discuss the change you wish to make via issue, | ||||||
|  | email, or any other method with the owners of this repository before making a change. | ||||||
|  |  | ||||||
|  | Please note we have a code of conduct, please follow it in all your interactions with the project. | ||||||
|  |  | ||||||
|  | # Coding Style | ||||||
|  | - We follow every [PSR-2](https://www.php-fig.org/psr/psr-2/) ... | ||||||
|  | - ... except camelCase, that's too bad, we use snake_case | ||||||
|  |  | ||||||
|  | ## Merge Request Process | ||||||
|  |  | ||||||
|  | 1. Ensure you strip any trailing spaces off | ||||||
|  | 2. Increase the version numbers in any examples files and the README.md to the new version that this | ||||||
|  |    Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). | ||||||
|  | 3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you | ||||||
|  |    do not have permission to do that, you may request the second reviewer to merge it for you. | ||||||
|  |  | ||||||
|  | ## Code of Conduct | ||||||
|  |  | ||||||
|  | ### Our Pledge | ||||||
|  |  | ||||||
|  | In the interest of fostering an open and welcoming environment, we as | ||||||
|  | contributors and maintainers pledge to making participation in our project and | ||||||
|  | our community a harassment-free experience for everyone, regardless of age, body | ||||||
|  | size, disability, ethnicity, gender identity and expression, level of experience, | ||||||
|  | nationality, personal appearance, race, religion, or sexual identity and | ||||||
|  | orientation. | ||||||
|  |  | ||||||
|  | ### Our Standards | ||||||
|  |  | ||||||
|  | Examples of behavior that contributes to creating a positive environment | ||||||
|  | include: | ||||||
|  |  | ||||||
|  | * Using welcoming and inclusive language | ||||||
|  | * Being respectful of differing viewpoints and experiences | ||||||
|  | * Gracefully accepting constructive criticism | ||||||
|  | * Focusing on what is best for the community | ||||||
|  | * Showing empathy towards other community members | ||||||
|  |  | ||||||
|  | Examples of unacceptable behavior by participants include: | ||||||
|  |  | ||||||
|  | * The use of sexualized language or imagery and unwelcome sexual attention or | ||||||
|  | advances | ||||||
|  | * Trolling, insulting/derogatory comments, and personal or political attacks | ||||||
|  | * Public or private harassment | ||||||
|  | * Publishing others' private information, such as a physical or electronic | ||||||
|  |   address, without explicit permission | ||||||
|  | * Other conduct which could reasonably be considered inappropriate in a | ||||||
|  |   professional setting | ||||||
|  |  | ||||||
|  | ### Our Responsibilities | ||||||
|  |  | ||||||
|  | Project maintainers are responsible for clarifying the standards of acceptable | ||||||
|  | behavior and are expected to take appropriate and fair corrective action in | ||||||
|  | response to any instances of unacceptable behavior. | ||||||
|  |  | ||||||
|  | Project maintainers have the right and responsibility to remove, edit, or | ||||||
|  | reject comments, commits, code, wiki edits, issues, and other contributions | ||||||
|  | that are not aligned to this Code of Conduct, or to ban temporarily or | ||||||
|  | permanently any contributor for other behaviors that they deem inappropriate, | ||||||
|  | threatening, offensive, or harmful. | ||||||
|  |  | ||||||
|  | ### Scope | ||||||
|  |  | ||||||
|  | This Code of Conduct applies both within project spaces and in public spaces | ||||||
|  | when an individual is representing the project or its community. Examples of | ||||||
|  | representing a project or community include using an official project e-mail | ||||||
|  | address, posting via an official social media account, or acting as an appointed | ||||||
|  | representative at an online or offline event. Representation of a project may be | ||||||
|  | further defined and clarified by project maintainers. | ||||||
|  |  | ||||||
|  | ### Enforcement | ||||||
|  |  | ||||||
|  | Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||||
|  | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All | ||||||
|  | complaints will be reviewed and investigated and will result in a response that | ||||||
|  | is deemed necessary and appropriate to the circumstances. The project team is | ||||||
|  | obligated to maintain confidentiality with regard to the reporter of an incident. | ||||||
|  | Further details of specific enforcement policies may be posted separately. | ||||||
|  |  | ||||||
|  | Project maintainers who do not follow or enforce the Code of Conduct in good | ||||||
|  | faith may face temporary or permanent repercussions as determined by other | ||||||
|  | members of the project's leadership. | ||||||
|  |  | ||||||
|  | ### Attribution | ||||||
|  |  | ||||||
|  | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, | ||||||
|  | available at [http://contributor-covenant.org/version/1/4][version] | ||||||
|  |  | ||||||
|  | [homepage]: http://contributor-covenant.org | ||||||
|  | [version]: http://contributor-covenant.org/version/1/4/ | ||||||
							
								
								
									
										661
									
								
								COPYING
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										661
									
								
								COPYING
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,661 @@ | |||||||
|  |                     GNU AFFERO GENERAL PUBLIC LICENSE | ||||||
|  |                        Version 3, 19 November 2007 | ||||||
|  |  | ||||||
|  |  Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> | ||||||
|  |  Everyone is permitted to copy and distribute verbatim copies | ||||||
|  |  of this license document, but changing it is not allowed. | ||||||
|  |  | ||||||
|  |                             Preamble | ||||||
|  |  | ||||||
|  |   The GNU Affero General Public License is a free, copyleft license for | ||||||
|  | software and other kinds of works, specifically designed to ensure | ||||||
|  | cooperation with the community in the case of network server software. | ||||||
|  |  | ||||||
|  |   The licenses for most software and other practical works are designed | ||||||
|  | to take away your freedom to share and change the works.  By contrast, | ||||||
|  | our General Public Licenses are intended to guarantee your freedom to | ||||||
|  | share and change all versions of a program--to make sure it remains free | ||||||
|  | software for all its users. | ||||||
|  |  | ||||||
|  |   When we speak of free software, we are referring to freedom, not | ||||||
|  | price.  Our General Public Licenses are designed to make sure that you | ||||||
|  | have the freedom to distribute copies of free software (and charge for | ||||||
|  | them if you wish), that you receive source code or can get it if you | ||||||
|  | want it, that you can change the software or use pieces of it in new | ||||||
|  | free programs, and that you know you can do these things. | ||||||
|  |  | ||||||
|  |   Developers that use our General Public Licenses protect your rights | ||||||
|  | with two steps: (1) assert copyright on the software, and (2) offer | ||||||
|  | you this License which gives you legal permission to copy, distribute | ||||||
|  | and/or modify the software. | ||||||
|  |  | ||||||
|  |   A secondary benefit of defending all users' freedom is that | ||||||
|  | improvements made in alternate versions of the program, if they | ||||||
|  | receive widespread use, become available for other developers to | ||||||
|  | incorporate.  Many developers of free software are heartened and | ||||||
|  | encouraged by the resulting cooperation.  However, in the case of | ||||||
|  | software used on network servers, this result may fail to come about. | ||||||
|  | The GNU General Public License permits making a modified version and | ||||||
|  | letting the public access it on a server without ever releasing its | ||||||
|  | source code to the public. | ||||||
|  |  | ||||||
|  |   The GNU Affero General Public License is designed specifically to | ||||||
|  | ensure that, in such cases, the modified source code becomes available | ||||||
|  | to the community.  It requires the operator of a network server to | ||||||
|  | provide the source code of the modified version running there to the | ||||||
|  | users of that server.  Therefore, public use of a modified version, on | ||||||
|  | a publicly accessible server, gives the public access to the source | ||||||
|  | code of the modified version. | ||||||
|  |  | ||||||
|  |   An older license, called the Affero General Public License and | ||||||
|  | published by Affero, was designed to accomplish similar goals.  This is | ||||||
|  | a different license, not a version of the Affero GPL, but Affero has | ||||||
|  | released a new version of the Affero GPL which permits relicensing under | ||||||
|  | this license. | ||||||
|  |  | ||||||
|  |   The precise terms and conditions for copying, distribution and | ||||||
|  | modification follow. | ||||||
|  |  | ||||||
|  |                        TERMS AND CONDITIONS | ||||||
|  |  | ||||||
|  |   0. Definitions. | ||||||
|  |  | ||||||
|  |   "This License" refers to version 3 of the GNU Affero General Public License. | ||||||
|  |  | ||||||
|  |   "Copyright" also means copyright-like laws that apply to other kinds of | ||||||
|  | works, such as semiconductor masks. | ||||||
|  |  | ||||||
|  |   "The Program" refers to any copyrightable work licensed under this | ||||||
|  | License.  Each licensee is addressed as "you".  "Licensees" and | ||||||
|  | "recipients" may be individuals or organizations. | ||||||
|  |  | ||||||
|  |   To "modify" a work means to copy from or adapt all or part of the work | ||||||
|  | in a fashion requiring copyright permission, other than the making of an | ||||||
|  | exact copy.  The resulting work is called a "modified version" of the | ||||||
|  | earlier work or a work "based on" the earlier work. | ||||||
|  |  | ||||||
|  |   A "covered work" means either the unmodified Program or a work based | ||||||
|  | on the Program. | ||||||
|  |  | ||||||
|  |   To "propagate" a work means to do anything with it that, without | ||||||
|  | permission, would make you directly or secondarily liable for | ||||||
|  | infringement under applicable copyright law, except executing it on a | ||||||
|  | computer or modifying a private copy.  Propagation includes copying, | ||||||
|  | distribution (with or without modification), making available to the | ||||||
|  | public, and in some countries other activities as well. | ||||||
|  |  | ||||||
|  |   To "convey" a work means any kind of propagation that enables other | ||||||
|  | parties to make or receive copies.  Mere interaction with a user through | ||||||
|  | a computer network, with no transfer of a copy, is not conveying. | ||||||
|  |  | ||||||
|  |   An interactive user interface displays "Appropriate Legal Notices" | ||||||
|  | to the extent that it includes a convenient and prominently visible | ||||||
|  | feature that (1) displays an appropriate copyright notice, and (2) | ||||||
|  | tells the user that there is no warranty for the work (except to the | ||||||
|  | extent that warranties are provided), that licensees may convey the | ||||||
|  | work under this License, and how to view a copy of this License.  If | ||||||
|  | the interface presents a list of user commands or options, such as a | ||||||
|  | menu, a prominent item in the list meets this criterion. | ||||||
|  |  | ||||||
|  |   1. Source Code. | ||||||
|  |  | ||||||
|  |   The "source code" for a work means the preferred form of the work | ||||||
|  | for making modifications to it.  "Object code" means any non-source | ||||||
|  | form of a work. | ||||||
|  |  | ||||||
|  |   A "Standard Interface" means an interface that either is an official | ||||||
|  | standard defined by a recognized standards body, or, in the case of | ||||||
|  | interfaces specified for a particular programming language, one that | ||||||
|  | is widely used among developers working in that language. | ||||||
|  |  | ||||||
|  |   The "System Libraries" of an executable work include anything, other | ||||||
|  | than the work as a whole, that (a) is included in the normal form of | ||||||
|  | packaging a Major Component, but which is not part of that Major | ||||||
|  | Component, and (b) serves only to enable use of the work with that | ||||||
|  | Major Component, or to implement a Standard Interface for which an | ||||||
|  | implementation is available to the public in source code form.  A | ||||||
|  | "Major Component", in this context, means a major essential component | ||||||
|  | (kernel, window system, and so on) of the specific operating system | ||||||
|  | (if any) on which the executable work runs, or a compiler used to | ||||||
|  | produce the work, or an object code interpreter used to run it. | ||||||
|  |  | ||||||
|  |   The "Corresponding Source" for a work in object code form means all | ||||||
|  | the source code needed to generate, install, and (for an executable | ||||||
|  | work) run the object code and to modify the work, including scripts to | ||||||
|  | control those activities.  However, it does not include the work's | ||||||
|  | System Libraries, or general-purpose tools or generally available free | ||||||
|  | programs which are used unmodified in performing those activities but | ||||||
|  | which are not part of the work.  For example, Corresponding Source | ||||||
|  | includes interface definition files associated with source files for | ||||||
|  | the work, and the source code for shared libraries and dynamically | ||||||
|  | linked subprograms that the work is specifically designed to require, | ||||||
|  | such as by intimate data communication or control flow between those | ||||||
|  | subprograms and other parts of the work. | ||||||
|  |  | ||||||
|  |   The Corresponding Source need not include anything that users | ||||||
|  | can regenerate automatically from other parts of the Corresponding | ||||||
|  | Source. | ||||||
|  |  | ||||||
|  |   The Corresponding Source for a work in source code form is that | ||||||
|  | same work. | ||||||
|  |  | ||||||
|  |   2. Basic Permissions. | ||||||
|  |  | ||||||
|  |   All rights granted under this License are granted for the term of | ||||||
|  | copyright on the Program, and are irrevocable provided the stated | ||||||
|  | conditions are met.  This License explicitly affirms your unlimited | ||||||
|  | permission to run the unmodified Program.  The output from running a | ||||||
|  | covered work is covered by this License only if the output, given its | ||||||
|  | content, constitutes a covered work.  This License acknowledges your | ||||||
|  | rights of fair use or other equivalent, as provided by copyright law. | ||||||
|  |  | ||||||
|  |   You may make, run and propagate covered works that you do not | ||||||
|  | convey, without conditions so long as your license otherwise remains | ||||||
|  | in force.  You may convey covered works to others for the sole purpose | ||||||
|  | of having them make modifications exclusively for you, or provide you | ||||||
|  | with facilities for running those works, provided that you comply with | ||||||
|  | the terms of this License in conveying all material for which you do | ||||||
|  | not control copyright.  Those thus making or running the covered works | ||||||
|  | for you must do so exclusively on your behalf, under your direction | ||||||
|  | and control, on terms that prohibit them from making any copies of | ||||||
|  | your copyrighted material outside their relationship with you. | ||||||
|  |  | ||||||
|  |   Conveying under any other circumstances is permitted solely under | ||||||
|  | the conditions stated below.  Sublicensing is not allowed; section 10 | ||||||
|  | makes it unnecessary. | ||||||
|  |  | ||||||
|  |   3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||||
|  |  | ||||||
|  |   No covered work shall be deemed part of an effective technological | ||||||
|  | measure under any applicable law fulfilling obligations under article | ||||||
|  | 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||||
|  | similar laws prohibiting or restricting circumvention of such | ||||||
|  | measures. | ||||||
|  |  | ||||||
|  |   When you convey a covered work, you waive any legal power to forbid | ||||||
|  | circumvention of technological measures to the extent such circumvention | ||||||
|  | is effected by exercising rights under this License with respect to | ||||||
|  | the covered work, and you disclaim any intention to limit operation or | ||||||
|  | modification of the work as a means of enforcing, against the work's | ||||||
|  | users, your or third parties' legal rights to forbid circumvention of | ||||||
|  | technological measures. | ||||||
|  |  | ||||||
|  |   4. Conveying Verbatim Copies. | ||||||
|  |  | ||||||
|  |   You may convey verbatim copies of the Program's source code as you | ||||||
|  | receive it, in any medium, provided that you conspicuously and | ||||||
|  | appropriately publish on each copy an appropriate copyright notice; | ||||||
|  | keep intact all notices stating that this License and any | ||||||
|  | non-permissive terms added in accord with section 7 apply to the code; | ||||||
|  | keep intact all notices of the absence of any warranty; and give all | ||||||
|  | recipients a copy of this License along with the Program. | ||||||
|  |  | ||||||
|  |   You may charge any price or no price for each copy that you convey, | ||||||
|  | and you may offer support or warranty protection for a fee. | ||||||
|  |  | ||||||
|  |   5. Conveying Modified Source Versions. | ||||||
|  |  | ||||||
|  |   You may convey a work based on the Program, or the modifications to | ||||||
|  | produce it from the Program, in the form of source code under the | ||||||
|  | terms of section 4, provided that you also meet all of these conditions: | ||||||
|  |  | ||||||
|  |     a) The work must carry prominent notices stating that you modified | ||||||
|  |     it, and giving a relevant date. | ||||||
|  |  | ||||||
|  |     b) The work must carry prominent notices stating that it is | ||||||
|  |     released under this License and any conditions added under section | ||||||
|  |     7.  This requirement modifies the requirement in section 4 to | ||||||
|  |     "keep intact all notices". | ||||||
|  |  | ||||||
|  |     c) You must license the entire work, as a whole, under this | ||||||
|  |     License to anyone who comes into possession of a copy.  This | ||||||
|  |     License will therefore apply, along with any applicable section 7 | ||||||
|  |     additional terms, to the whole of the work, and all its parts, | ||||||
|  |     regardless of how they are packaged.  This License gives no | ||||||
|  |     permission to license the work in any other way, but it does not | ||||||
|  |     invalidate such permission if you have separately received it. | ||||||
|  |  | ||||||
|  |     d) If the work has interactive user interfaces, each must display | ||||||
|  |     Appropriate Legal Notices; however, if the Program has interactive | ||||||
|  |     interfaces that do not display Appropriate Legal Notices, your | ||||||
|  |     work need not make them do so. | ||||||
|  |  | ||||||
|  |   A compilation of a covered work with other separate and independent | ||||||
|  | works, which are not by their nature extensions of the covered work, | ||||||
|  | and which are not combined with it such as to form a larger program, | ||||||
|  | in or on a volume of a storage or distribution medium, is called an | ||||||
|  | "aggregate" if the compilation and its resulting copyright are not | ||||||
|  | used to limit the access or legal rights of the compilation's users | ||||||
|  | beyond what the individual works permit.  Inclusion of a covered work | ||||||
|  | in an aggregate does not cause this License to apply to the other | ||||||
|  | parts of the aggregate. | ||||||
|  |  | ||||||
|  |   6. Conveying Non-Source Forms. | ||||||
|  |  | ||||||
|  |   You may convey a covered work in object code form under the terms | ||||||
|  | of sections 4 and 5, provided that you also convey the | ||||||
|  | machine-readable Corresponding Source under the terms of this License, | ||||||
|  | in one of these ways: | ||||||
|  |  | ||||||
|  |     a) Convey the object code in, or embodied in, a physical product | ||||||
|  |     (including a physical distribution medium), accompanied by the | ||||||
|  |     Corresponding Source fixed on a durable physical medium | ||||||
|  |     customarily used for software interchange. | ||||||
|  |  | ||||||
|  |     b) Convey the object code in, or embodied in, a physical product | ||||||
|  |     (including a physical distribution medium), accompanied by a | ||||||
|  |     written offer, valid for at least three years and valid for as | ||||||
|  |     long as you offer spare parts or customer support for that product | ||||||
|  |     model, to give anyone who possesses the object code either (1) a | ||||||
|  |     copy of the Corresponding Source for all the software in the | ||||||
|  |     product that is covered by this License, on a durable physical | ||||||
|  |     medium customarily used for software interchange, for a price no | ||||||
|  |     more than your reasonable cost of physically performing this | ||||||
|  |     conveying of source, or (2) access to copy the | ||||||
|  |     Corresponding Source from a network server at no charge. | ||||||
|  |  | ||||||
|  |     c) Convey individual copies of the object code with a copy of the | ||||||
|  |     written offer to provide the Corresponding Source.  This | ||||||
|  |     alternative is allowed only occasionally and noncommercially, and | ||||||
|  |     only if you received the object code with such an offer, in accord | ||||||
|  |     with subsection 6b. | ||||||
|  |  | ||||||
|  |     d) Convey the object code by offering access from a designated | ||||||
|  |     place (gratis or for a charge), and offer equivalent access to the | ||||||
|  |     Corresponding Source in the same way through the same place at no | ||||||
|  |     further charge.  You need not require recipients to copy the | ||||||
|  |     Corresponding Source along with the object code.  If the place to | ||||||
|  |     copy the object code is a network server, the Corresponding Source | ||||||
|  |     may be on a different server (operated by you or a third party) | ||||||
|  |     that supports equivalent copying facilities, provided you maintain | ||||||
|  |     clear directions next to the object code saying where to find the | ||||||
|  |     Corresponding Source.  Regardless of what server hosts the | ||||||
|  |     Corresponding Source, you remain obligated to ensure that it is | ||||||
|  |     available for as long as needed to satisfy these requirements. | ||||||
|  |  | ||||||
|  |     e) Convey the object code using peer-to-peer transmission, provided | ||||||
|  |     you inform other peers where the object code and Corresponding | ||||||
|  |     Source of the work are being offered to the general public at no | ||||||
|  |     charge under subsection 6d. | ||||||
|  |  | ||||||
|  |   A separable portion of the object code, whose source code is excluded | ||||||
|  | from the Corresponding Source as a System Library, need not be | ||||||
|  | included in conveying the object code work. | ||||||
|  |  | ||||||
|  |   A "User Product" is either (1) a "consumer product", which means any | ||||||
|  | tangible personal property which is normally used for personal, family, | ||||||
|  | or household purposes, or (2) anything designed or sold for incorporation | ||||||
|  | into a dwelling.  In determining whether a product is a consumer product, | ||||||
|  | doubtful cases shall be resolved in favor of coverage.  For a particular | ||||||
|  | product received by a particular user, "normally used" refers to a | ||||||
|  | typical or common use of that class of product, regardless of the status | ||||||
|  | of the particular user or of the way in which the particular user | ||||||
|  | actually uses, or expects or is expected to use, the product.  A product | ||||||
|  | is a consumer product regardless of whether the product has substantial | ||||||
|  | commercial, industrial or non-consumer uses, unless such uses represent | ||||||
|  | the only significant mode of use of the product. | ||||||
|  |  | ||||||
|  |   "Installation Information" for a User Product means any methods, | ||||||
|  | procedures, authorization keys, or other information required to install | ||||||
|  | and execute modified versions of a covered work in that User Product from | ||||||
|  | a modified version of its Corresponding Source.  The information must | ||||||
|  | suffice to ensure that the continued functioning of the modified object | ||||||
|  | code is in no case prevented or interfered with solely because | ||||||
|  | modification has been made. | ||||||
|  |  | ||||||
|  |   If you convey an object code work under this section in, or with, or | ||||||
|  | specifically for use in, a User Product, and the conveying occurs as | ||||||
|  | part of a transaction in which the right of possession and use of the | ||||||
|  | User Product is transferred to the recipient in perpetuity or for a | ||||||
|  | fixed term (regardless of how the transaction is characterized), the | ||||||
|  | Corresponding Source conveyed under this section must be accompanied | ||||||
|  | by the Installation Information.  But this requirement does not apply | ||||||
|  | if neither you nor any third party retains the ability to install | ||||||
|  | modified object code on the User Product (for example, the work has | ||||||
|  | been installed in ROM). | ||||||
|  |  | ||||||
|  |   The requirement to provide Installation Information does not include a | ||||||
|  | requirement to continue to provide support service, warranty, or updates | ||||||
|  | for a work that has been modified or installed by the recipient, or for | ||||||
|  | the User Product in which it has been modified or installed.  Access to a | ||||||
|  | network may be denied when the modification itself materially and | ||||||
|  | adversely affects the operation of the network or violates the rules and | ||||||
|  | protocols for communication across the network. | ||||||
|  |  | ||||||
|  |   Corresponding Source conveyed, and Installation Information provided, | ||||||
|  | in accord with this section must be in a format that is publicly | ||||||
|  | documented (and with an implementation available to the public in | ||||||
|  | source code form), and must require no special password or key for | ||||||
|  | unpacking, reading or copying. | ||||||
|  |  | ||||||
|  |   7. Additional Terms. | ||||||
|  |  | ||||||
|  |   "Additional permissions" are terms that supplement the terms of this | ||||||
|  | License by making exceptions from one or more of its conditions. | ||||||
|  | Additional permissions that are applicable to the entire Program shall | ||||||
|  | be treated as though they were included in this License, to the extent | ||||||
|  | that they are valid under applicable law.  If additional permissions | ||||||
|  | apply only to part of the Program, that part may be used separately | ||||||
|  | under those permissions, but the entire Program remains governed by | ||||||
|  | this License without regard to the additional permissions. | ||||||
|  |  | ||||||
|  |   When you convey a copy of a covered work, you may at your option | ||||||
|  | remove any additional permissions from that copy, or from any part of | ||||||
|  | it.  (Additional permissions may be written to require their own | ||||||
|  | removal in certain cases when you modify the work.)  You may place | ||||||
|  | additional permissions on material, added by you to a covered work, | ||||||
|  | for which you have or can give appropriate copyright permission. | ||||||
|  |  | ||||||
|  |   Notwithstanding any other provision of this License, for material you | ||||||
|  | add to a covered work, you may (if authorized by the copyright holders of | ||||||
|  | that material) supplement the terms of this License with terms: | ||||||
|  |  | ||||||
|  |     a) Disclaiming warranty or limiting liability differently from the | ||||||
|  |     terms of sections 15 and 16 of this License; or | ||||||
|  |  | ||||||
|  |     b) Requiring preservation of specified reasonable legal notices or | ||||||
|  |     author attributions in that material or in the Appropriate Legal | ||||||
|  |     Notices displayed by works containing it; or | ||||||
|  |  | ||||||
|  |     c) Prohibiting misrepresentation of the origin of that material, or | ||||||
|  |     requiring that modified versions of such material be marked in | ||||||
|  |     reasonable ways as different from the original version; or | ||||||
|  |  | ||||||
|  |     d) Limiting the use for publicity purposes of names of licensors or | ||||||
|  |     authors of the material; or | ||||||
|  |  | ||||||
|  |     e) Declining to grant rights under trademark law for use of some | ||||||
|  |     trade names, trademarks, or service marks; or | ||||||
|  |  | ||||||
|  |     f) Requiring indemnification of licensors and authors of that | ||||||
|  |     material by anyone who conveys the material (or modified versions of | ||||||
|  |     it) with contractual assumptions of liability to the recipient, for | ||||||
|  |     any liability that these contractual assumptions directly impose on | ||||||
|  |     those licensors and authors. | ||||||
|  |  | ||||||
|  |   All other non-permissive additional terms are considered "further | ||||||
|  | restrictions" within the meaning of section 10.  If the Program as you | ||||||
|  | received it, or any part of it, contains a notice stating that it is | ||||||
|  | governed by this License along with a term that is a further | ||||||
|  | restriction, you may remove that term.  If a license document contains | ||||||
|  | a further restriction but permits relicensing or conveying under this | ||||||
|  | License, you may add to a covered work material governed by the terms | ||||||
|  | of that license document, provided that the further restriction does | ||||||
|  | not survive such relicensing or conveying. | ||||||
|  |  | ||||||
|  |   If you add terms to a covered work in accord with this section, you | ||||||
|  | must place, in the relevant source files, a statement of the | ||||||
|  | additional terms that apply to those files, or a notice indicating | ||||||
|  | where to find the applicable terms. | ||||||
|  |  | ||||||
|  |   Additional terms, permissive or non-permissive, may be stated in the | ||||||
|  | form of a separately written license, or stated as exceptions; | ||||||
|  | the above requirements apply either way. | ||||||
|  |  | ||||||
|  |   8. Termination. | ||||||
|  |  | ||||||
|  |   You may not propagate or modify a covered work except as expressly | ||||||
|  | provided under this License.  Any attempt otherwise to propagate or | ||||||
|  | modify it is void, and will automatically terminate your rights under | ||||||
|  | this License (including any patent licenses granted under the third | ||||||
|  | paragraph of section 11). | ||||||
|  |  | ||||||
|  |   However, if you cease all violation of this License, then your | ||||||
|  | license from a particular copyright holder is reinstated (a) | ||||||
|  | provisionally, unless and until the copyright holder explicitly and | ||||||
|  | finally terminates your license, and (b) permanently, if the copyright | ||||||
|  | holder fails to notify you of the violation by some reasonable means | ||||||
|  | prior to 60 days after the cessation. | ||||||
|  |  | ||||||
|  |   Moreover, your license from a particular copyright holder is | ||||||
|  | reinstated permanently if the copyright holder notifies you of the | ||||||
|  | violation by some reasonable means, this is the first time you have | ||||||
|  | received notice of violation of this License (for any work) from that | ||||||
|  | copyright holder, and you cure the violation prior to 30 days after | ||||||
|  | your receipt of the notice. | ||||||
|  |  | ||||||
|  |   Termination of your rights under this section does not terminate the | ||||||
|  | licenses of parties who have received copies or rights from you under | ||||||
|  | this License.  If your rights have been terminated and not permanently | ||||||
|  | reinstated, you do not qualify to receive new licenses for the same | ||||||
|  | material under section 10. | ||||||
|  |  | ||||||
|  |   9. Acceptance Not Required for Having Copies. | ||||||
|  |  | ||||||
|  |   You are not required to accept this License in order to receive or | ||||||
|  | run a copy of the Program.  Ancillary propagation of a covered work | ||||||
|  | occurring solely as a consequence of using peer-to-peer transmission | ||||||
|  | to receive a copy likewise does not require acceptance.  However, | ||||||
|  | nothing other than this License grants you permission to propagate or | ||||||
|  | modify any covered work.  These actions infringe copyright if you do | ||||||
|  | not accept this License.  Therefore, by modifying or propagating a | ||||||
|  | covered work, you indicate your acceptance of this License to do so. | ||||||
|  |  | ||||||
|  |   10. Automatic Licensing of Downstream Recipients. | ||||||
|  |  | ||||||
|  |   Each time you convey a covered work, the recipient automatically | ||||||
|  | receives a license from the original licensors, to run, modify and | ||||||
|  | propagate that work, subject to this License.  You are not responsible | ||||||
|  | for enforcing compliance by third parties with this License. | ||||||
|  |  | ||||||
|  |   An "entity transaction" is a transaction transferring control of an | ||||||
|  | organization, or substantially all assets of one, or subdividing an | ||||||
|  | organization, or merging organizations.  If propagation of a covered | ||||||
|  | work results from an entity transaction, each party to that | ||||||
|  | transaction who receives a copy of the work also receives whatever | ||||||
|  | licenses to the work the party's predecessor in interest had or could | ||||||
|  | give under the previous paragraph, plus a right to possession of the | ||||||
|  | Corresponding Source of the work from the predecessor in interest, if | ||||||
|  | the predecessor has it or can get it with reasonable efforts. | ||||||
|  |  | ||||||
|  |   You may not impose any further restrictions on the exercise of the | ||||||
|  | rights granted or affirmed under this License.  For example, you may | ||||||
|  | not impose a license fee, royalty, or other charge for exercise of | ||||||
|  | rights granted under this License, and you may not initiate litigation | ||||||
|  | (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||||
|  | any patent claim is infringed by making, using, selling, offering for | ||||||
|  | sale, or importing the Program or any portion of it. | ||||||
|  |  | ||||||
|  |   11. Patents. | ||||||
|  |  | ||||||
|  |   A "contributor" is a copyright holder who authorizes use under this | ||||||
|  | License of the Program or a work on which the Program is based.  The | ||||||
|  | work thus licensed is called the contributor's "contributor version". | ||||||
|  |  | ||||||
|  |   A contributor's "essential patent claims" are all patent claims | ||||||
|  | owned or controlled by the contributor, whether already acquired or | ||||||
|  | hereafter acquired, that would be infringed by some manner, permitted | ||||||
|  | by this License, of making, using, or selling its contributor version, | ||||||
|  | but do not include claims that would be infringed only as a | ||||||
|  | consequence of further modification of the contributor version.  For | ||||||
|  | purposes of this definition, "control" includes the right to grant | ||||||
|  | patent sublicenses in a manner consistent with the requirements of | ||||||
|  | this License. | ||||||
|  |  | ||||||
|  |   Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||||
|  | patent license under the contributor's essential patent claims, to | ||||||
|  | make, use, sell, offer for sale, import and otherwise run, modify and | ||||||
|  | propagate the contents of its contributor version. | ||||||
|  |  | ||||||
|  |   In the following three paragraphs, a "patent license" is any express | ||||||
|  | agreement or commitment, however denominated, not to enforce a patent | ||||||
|  | (such as an express permission to practice a patent or covenant not to | ||||||
|  | sue for patent infringement).  To "grant" such a patent license to a | ||||||
|  | party means to make such an agreement or commitment not to enforce a | ||||||
|  | patent against the party. | ||||||
|  |  | ||||||
|  |   If you convey a covered work, knowingly relying on a patent license, | ||||||
|  | and the Corresponding Source of the work is not available for anyone | ||||||
|  | to copy, free of charge and under the terms of this License, through a | ||||||
|  | publicly available network server or other readily accessible means, | ||||||
|  | then you must either (1) cause the Corresponding Source to be so | ||||||
|  | available, or (2) arrange to deprive yourself of the benefit of the | ||||||
|  | patent license for this particular work, or (3) arrange, in a manner | ||||||
|  | consistent with the requirements of this License, to extend the patent | ||||||
|  | license to downstream recipients.  "Knowingly relying" means you have | ||||||
|  | actual knowledge that, but for the patent license, your conveying the | ||||||
|  | covered work in a country, or your recipient's use of the covered work | ||||||
|  | in a country, would infringe one or more identifiable patents in that | ||||||
|  | country that you have reason to believe are valid. | ||||||
|  |  | ||||||
|  |   If, pursuant to or in connection with a single transaction or | ||||||
|  | arrangement, you convey, or propagate by procuring conveyance of, a | ||||||
|  | covered work, and grant a patent license to some of the parties | ||||||
|  | receiving the covered work authorizing them to use, propagate, modify | ||||||
|  | or convey a specific copy of the covered work, then the patent license | ||||||
|  | you grant is automatically extended to all recipients of the covered | ||||||
|  | work and works based on it. | ||||||
|  |  | ||||||
|  |   A patent license is "discriminatory" if it does not include within | ||||||
|  | the scope of its coverage, prohibits the exercise of, or is | ||||||
|  | conditioned on the non-exercise of one or more of the rights that are | ||||||
|  | specifically granted under this License.  You may not convey a covered | ||||||
|  | work if you are a party to an arrangement with a third party that is | ||||||
|  | in the business of distributing software, under which you make payment | ||||||
|  | to the third party based on the extent of your activity of conveying | ||||||
|  | the work, and under which the third party grants, to any of the | ||||||
|  | parties who would receive the covered work from you, a discriminatory | ||||||
|  | patent license (a) in connection with copies of the covered work | ||||||
|  | conveyed by you (or copies made from those copies), or (b) primarily | ||||||
|  | for and in connection with specific products or compilations that | ||||||
|  | contain the covered work, unless you entered into that arrangement, | ||||||
|  | or that patent license was granted, prior to 28 March 2007. | ||||||
|  |  | ||||||
|  |   Nothing in this License shall be construed as excluding or limiting | ||||||
|  | any implied license or other defenses to infringement that may | ||||||
|  | otherwise be available to you under applicable patent law. | ||||||
|  |  | ||||||
|  |   12. No Surrender of Others' Freedom. | ||||||
|  |  | ||||||
|  |   If conditions are imposed on you (whether by court order, agreement or | ||||||
|  | otherwise) that contradict the conditions of this License, they do not | ||||||
|  | excuse you from the conditions of this License.  If you cannot convey a | ||||||
|  | covered work so as to satisfy simultaneously your obligations under this | ||||||
|  | License and any other pertinent obligations, then as a consequence you may | ||||||
|  | not convey it at all.  For example, if you agree to terms that obligate you | ||||||
|  | to collect a royalty for further conveying from those to whom you convey | ||||||
|  | the Program, the only way you could satisfy both those terms and this | ||||||
|  | License would be to refrain entirely from conveying the Program. | ||||||
|  |  | ||||||
|  |   13. Remote Network Interaction; Use with the GNU General Public License. | ||||||
|  |  | ||||||
|  |   Notwithstanding any other provision of this License, if you modify the | ||||||
|  | Program, your modified version must prominently offer all users | ||||||
|  | interacting with it remotely through a computer network (if your version | ||||||
|  | supports such interaction) an opportunity to receive the Corresponding | ||||||
|  | Source of your version by providing access to the Corresponding Source | ||||||
|  | from a network server at no charge, through some standard or customary | ||||||
|  | means of facilitating copying of software.  This Corresponding Source | ||||||
|  | shall include the Corresponding Source for any work covered by version 3 | ||||||
|  | of the GNU General Public License that is incorporated pursuant to the | ||||||
|  | following paragraph. | ||||||
|  |  | ||||||
|  |   Notwithstanding any other provision of this License, you have | ||||||
|  | permission to link or combine any covered work with a work licensed | ||||||
|  | under version 3 of the GNU General Public License into a single | ||||||
|  | combined work, and to convey the resulting work.  The terms of this | ||||||
|  | License will continue to apply to the part which is the covered work, | ||||||
|  | but the work with which it is combined will remain governed by version | ||||||
|  | 3 of the GNU General Public License. | ||||||
|  |  | ||||||
|  |   14. Revised Versions of this License. | ||||||
|  |  | ||||||
|  |   The Free Software Foundation may publish revised and/or new versions of | ||||||
|  | the GNU Affero General Public License from time to time.  Such new versions | ||||||
|  | will be similar in spirit to the present version, but may differ in detail to | ||||||
|  | address new problems or concerns. | ||||||
|  |  | ||||||
|  |   Each version is given a distinguishing version number.  If the | ||||||
|  | Program specifies that a certain numbered version of the GNU Affero General | ||||||
|  | Public License "or any later version" applies to it, you have the | ||||||
|  | option of following the terms and conditions either of that numbered | ||||||
|  | version or of any later version published by the Free Software | ||||||
|  | Foundation.  If the Program does not specify a version number of the | ||||||
|  | GNU Affero General Public License, you may choose any version ever published | ||||||
|  | by the Free Software Foundation. | ||||||
|  |  | ||||||
|  |   If the Program specifies that a proxy can decide which future | ||||||
|  | versions of the GNU Affero General Public License can be used, that proxy's | ||||||
|  | public statement of acceptance of a version permanently authorizes you | ||||||
|  | to choose that version for the Program. | ||||||
|  |  | ||||||
|  |   Later license versions may give you additional or different | ||||||
|  | permissions.  However, no additional obligations are imposed on any | ||||||
|  | author or copyright holder as a result of your choosing to follow a | ||||||
|  | later version. | ||||||
|  |  | ||||||
|  |   15. Disclaimer of Warranty. | ||||||
|  |  | ||||||
|  |   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||||
|  | APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||||
|  | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||||||
|  | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||||||
|  | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||||
|  | PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||||||
|  | IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||||||
|  | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||||
|  |  | ||||||
|  |   16. Limitation of Liability. | ||||||
|  |  | ||||||
|  |   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||||
|  | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||||||
|  | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||||
|  | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||||||
|  | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||||||
|  | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||||||
|  | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||||||
|  | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||||||
|  | SUCH DAMAGES. | ||||||
|  |  | ||||||
|  |   17. Interpretation of Sections 15 and 16. | ||||||
|  |  | ||||||
|  |   If the disclaimer of warranty and limitation of liability provided | ||||||
|  | above cannot be given local legal effect according to their terms, | ||||||
|  | reviewing courts shall apply local law that most closely approximates | ||||||
|  | an absolute waiver of all civil liability in connection with the | ||||||
|  | Program, unless a warranty or assumption of liability accompanies a | ||||||
|  | copy of the Program in return for a fee. | ||||||
|  |  | ||||||
|  |                      END OF TERMS AND CONDITIONS | ||||||
|  |  | ||||||
|  |             How to Apply These Terms to Your New Programs | ||||||
|  |  | ||||||
|  |   If you develop a new program, and you want it to be of the greatest | ||||||
|  | possible use to the public, the best way to achieve this is to make it | ||||||
|  | free software which everyone can redistribute and change under these terms. | ||||||
|  |  | ||||||
|  |   To do so, attach the following notices to the program.  It is safest | ||||||
|  | to attach them to the start of each source file to most effectively | ||||||
|  | state the exclusion of warranty; and each file should have at least | ||||||
|  | the "copyright" line and a pointer to where the full notice is found. | ||||||
|  |  | ||||||
|  |     <one line to give the program's name and a brief idea of what it does.> | ||||||
|  |     Copyright (C) <year>  <name of author> | ||||||
|  |  | ||||||
|  |     This program is free software: you can redistribute it and/or modify | ||||||
|  |     it under the terms of the GNU Affero General Public License as published by | ||||||
|  |     the Free Software Foundation, either version 3 of the License, or | ||||||
|  |     (at your option) any later version. | ||||||
|  |  | ||||||
|  |     This program is distributed in the hope that it will be useful, | ||||||
|  |     but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |     GNU Affero General Public License for more details. | ||||||
|  |  | ||||||
|  |     You should have received a copy of the GNU Affero General Public License | ||||||
|  |     along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | Also add information on how to contact you by electronic and paper mail. | ||||||
|  |  | ||||||
|  |   If your software can interact with users remotely through a computer | ||||||
|  | network, you should also make sure that it provides a way for users to | ||||||
|  | get its source.  For example, if your program is a web application, its | ||||||
|  | interface could display a "Source" link that leads users to an archive | ||||||
|  | of the code.  There are many ways you could offer source, and different | ||||||
|  | solutions will be better for different programs; see section 13 for the | ||||||
|  | specific requirements. | ||||||
|  |  | ||||||
|  |   You should also get your employer (if you work as a programmer) or school, | ||||||
|  | if any, to sign a "copyright disclaimer" for the program, if necessary. | ||||||
|  | For more information on this, and how to apply and follow the GNU AGPL, see | ||||||
|  | <http://www.gnu.org/licenses/>. | ||||||
							
								
								
									
										75
									
								
								README.md
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										75
									
								
								README.md
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,2 +1,73 @@ | |||||||
| # ActivityPub GNU/Social Plugin | # ActivityPub plugin for GNU Social 1.0 Alpha | ||||||
| Warning: This is still a work in progress, not every ActivityPub property is fully implemented yet. | 2018 | ||||||
|  |  | ||||||
|  | (c) Free Software Foundation, Inc | ||||||
|  |  | ||||||
|  | This is the README file for GNU Social's ActivityPub plugin. | ||||||
|  | It includes general information about the plugin. | ||||||
|  |  | ||||||
|  | ## About | ||||||
|  |  | ||||||
|  | This plugin adds [ActivityPub](https://www.w3.org/TR/activitypub/) support to | ||||||
|  | GNU Social. | ||||||
|  |  | ||||||
|  | ## Setup | ||||||
|  |  | ||||||
|  | 1. Put all files in /plugins/ActivityPub | ||||||
|  |  | ||||||
|  | 2. Add `addPlugin('ActivityPub');` to your /config.php file. | ||||||
|  |  | ||||||
|  | 3. For better performance consider both: | ||||||
|  |    - disabling checkschema (instructions in GNU social's config.php), but don't forget to run it when updating plugins (including ActivityPub plugin) | ||||||
|  |    - installing Chimo's Redis plugin | ||||||
|  |  | ||||||
|  | ##  For testing, (shouldn't be used in production) | ||||||
|  |  | ||||||
|  |         composer install && vendor/bin/phpunit | ||||||
|  |  | ||||||
|  | ## Built With | ||||||
|  |  | ||||||
|  | * [PHPUnit](https://phpunit.de/) - Automated tests | ||||||
|  |  | ||||||
|  | ## Contributing | ||||||
|  |  | ||||||
|  | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting merge requests to us. | ||||||
|  |  | ||||||
|  | ## Versioning | ||||||
|  |  | ||||||
|  | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://git.gnu.io/gnu/GS-ActivityPub-Plugin/tags). | ||||||
|  |  | ||||||
|  | ## Credits | ||||||
|  |  | ||||||
|  | * **[Diogo Cordeiro](https://www.diogo.site/)** | ||||||
|  |  | ||||||
|  | See also the list of [contributors](https://git.gnu.io/gnu/GS-ActivityPub-Plugin/contributors) who participated in this project. | ||||||
|  |  | ||||||
|  | ## Extra special thanks | ||||||
|  |  | ||||||
|  | * **[Daniel Supernault](https://github.com/dansup)** | ||||||
|  | * **[Mikael Nordfeldth](https://mmn-o.se/)** | ||||||
|  |  | ||||||
|  | ## License | ||||||
|  |  | ||||||
|  | This program is free software: you can redistribute it and/or modify | ||||||
|  | it under the terms of the GNU Affero General Public License as | ||||||
|  | published by the Free Software Foundation, either version 3 of the | ||||||
|  | License, or (at your option) any later version. | ||||||
|  |  | ||||||
|  | This program is distributed in the hope that it will be useful, but | ||||||
|  | WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||||
|  | Affero General Public License for more details. | ||||||
|  |  | ||||||
|  | You should have received a copy of the GNU Affero General Public | ||||||
|  | License along with this program, in the file "COPYING".  If not, see | ||||||
|  | <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  |     IMPORTANT NOTE: The GNU Affero General Public License (AGPL) has | ||||||
|  |     *different requirements* from the "regular" GPL. In particular, if | ||||||
|  |     you make modifications to the plugin source code on your server, | ||||||
|  |     you *MUST MAKE AVAILABLE* the modified version of the source code | ||||||
|  |     to your users under the same license. This is a legal requirement | ||||||
|  |     of using the software, and if you do not wish to share your | ||||||
|  |     modifications, *YOU MAY NOT USE THIS PLUGIN*. | ||||||
|   | |||||||
							
								
								
									
										165
									
								
								actions/apactorfollowers.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										165
									
								
								actions/apactorfollowers.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,13 +20,12 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -40,75 +39,97 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  */ |  */ | ||||||
| class apActorFollowersAction extends ManagedAction | class apActorFollowersAction extends ManagedAction | ||||||
| { | { | ||||||
|         protected $needLogin = false; |     protected $needLogin = false; | ||||||
|         protected $canPost   = true; |     protected $canPost   = true; | ||||||
|  |  | ||||||
|         /** |     /** | ||||||
|          * Handle the Followers Collection request |      * Handle the Followers Collection request | ||||||
|          * |      * | ||||||
|          * @return void |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          */ |      * @return void | ||||||
|         protected function handle () |      */ | ||||||
|         { |     protected function handle() | ||||||
|                 $nickname = $this->trimmed ('nickname'); |     { | ||||||
|                 try { |         try { | ||||||
|                         $user    = User::getByNickname ($nickname); |             $profile = Profile::getByID($this->trimmed('id')); | ||||||
|                         $profile = $user->getProfile (); |             $profile_id = $profile->getID(); | ||||||
|                         $url     = $profile->profileurl; |         } catch (Exception $e) { | ||||||
|                 } catch (Exception $e) { |             ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||||
|                         ActivityPubReturn::error ('Invalid username.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (!isset ($_GET["page"])) { |  | ||||||
|                         $page = 1; |  | ||||||
|                 } else { |  | ||||||
|                         $page = intval ($this->trimmed ('page')); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if ($page <= 0) { |  | ||||||
|                         ActivityPubReturn::error ('Invalid page number.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 /* Fetch Followers */ |  | ||||||
|                 try { |  | ||||||
|                         $since = ($page - 1) * PROFILES_PER_MINILIST; |  | ||||||
|                         $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; |  | ||||||
|                         $sub   = $profile->getSubscribers ($since, $limit); |  | ||||||
|                 } catch (NoResultException $e) { |  | ||||||
|                         ActivityPubReturn::error ('This user has no followers.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 /* Calculate total items */ |  | ||||||
|                 $total_subs  = $profile->subscriberCount (); |  | ||||||
|                 $total_pages = ceil ($total_subs / PROFILES_PER_MINILIST); |  | ||||||
|  |  | ||||||
|                 if ($total_pages == 0) { |  | ||||||
|                         ActivityPubReturn::error ('This user has no followers.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if ($page > $total_pages) { |  | ||||||
|                         ActivityPubReturn::error ("There are only {$total_pages} pages."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 /* Get followers' URLs */ |  | ||||||
|                 $subs = array (); |  | ||||||
|                 while ($sub->fetch ()) { |  | ||||||
|                         $subs[] = $sub->profileurl; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $res = [ |  | ||||||
|                   '@context'     => [ |  | ||||||
|                     "https://www.w3.org/ns/activitystreams", |  | ||||||
|                     "https://w3id.org/security/v1", |  | ||||||
|                   ], |  | ||||||
|                   'id'           => "{$url}/followers.json", |  | ||||||
|                   'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), |  | ||||||
|                   'totalItems'   => $total_subs, |  | ||||||
|                   'next'         => $page+1 > $total_pages ? null : "{$url}/followers.json?page=".($page+1 == 1 ? 2 : $page+1), |  | ||||||
|                   'prev'         => $page == 1 ? null : "{$url}/followers.json?page=".($page-1 <= 0 ? 1 : $page-1), |  | ||||||
|                   'orderedItems' => $subs |  | ||||||
|                 ]; |  | ||||||
|  |  | ||||||
|                 ActivityPubReturn::answer ($res); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!$profile->isLocal()) { | ||||||
|  |             ActivityPubReturn::error("This is not a local user.", 403); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!isset($_GET["page"])) { | ||||||
|  |             $page = 0; | ||||||
|  |         } else { | ||||||
|  |             $page = intval($this->trimmed('page')); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if ($page < 0) { | ||||||
|  |             ActivityPubReturn::error('Invalid page number.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $since = ($page - 1) * PROFILES_PER_MINILIST; | ||||||
|  |         $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; | ||||||
|  |  | ||||||
|  |         /* Calculate total items */ | ||||||
|  |         $total_subs  = $profile->subscriberCount(); | ||||||
|  |         $total_pages = ceil($total_subs / PROFILES_PER_MINILIST); | ||||||
|  |  | ||||||
|  |         $res = [ | ||||||
|  |             '@context'     => [ | ||||||
|  |               "https://www.w3.org/ns/activitystreams", | ||||||
|  |               "https://w3id.org/security/v1", | ||||||
|  |             ], | ||||||
|  |             'id'           => common_local_url('apActorFollowers', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), | ||||||
|  |             'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), | ||||||
|  |             'totalItems'   => $total_subs | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         if ($page == 0) { | ||||||
|  |             $res['first'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'?page=1'; | ||||||
|  |         } else { | ||||||
|  |             $res['orderedItems'] = $this->generate_followers($profile, $since, $limit); | ||||||
|  |             $res['partOf'] = common_local_url('apActorFollowers', ['id' => $profile_id]); | ||||||
|  |  | ||||||
|  |             if ($page+1 < $total_pages) { | ||||||
|  |                 $res['next'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ($page > 1) { | ||||||
|  |                 $res['prev'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ActivityPubReturn::answer($res); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generates a list of stalkers for a given profile. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Profile $profile | ||||||
|  |      * @param int32 $since | ||||||
|  |      * @param int32 $limit | ||||||
|  |      * @return Array of URIs | ||||||
|  |      */ | ||||||
|  |     public static function generate_followers($profile, $since, $limit) | ||||||
|  |     { | ||||||
|  |         /* Fetch Followers */ | ||||||
|  |         try { | ||||||
|  |             $sub = $profile->getSubscribers($since, $limit); | ||||||
|  |         } catch (NoResultException $e) { | ||||||
|  |             // Just let the exception go on its merry way | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* Get followers' URLs */ | ||||||
|  |         $subs = []; | ||||||
|  |         while ($sub->fetch()) { | ||||||
|  |             $subs[] = ActivityPubPlugin::actor_uri($sub); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $subs; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										164
									
								
								actions/apactorfollowing.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										164
									
								
								actions/apactorfollowing.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,13 +20,12 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -40,75 +39,96 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  */ |  */ | ||||||
| class apActorFollowingAction extends ManagedAction | class apActorFollowingAction extends ManagedAction | ||||||
| { | { | ||||||
|         protected $needLogin = false; |     protected $needLogin = false; | ||||||
|         protected $canPost   = true; |     protected $canPost   = true; | ||||||
|  |  | ||||||
|         /** |     /** | ||||||
|          * Handle the Following Collection request |      * Handle the Following Collection request | ||||||
|          * |      * | ||||||
|          * @return void |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          */ |      * @return void | ||||||
|         protected function handle () |      */ | ||||||
|         { |     protected function handle() | ||||||
|                 $nickname = $this->trimmed ('nickname'); |     { | ||||||
|                 try { |         try { | ||||||
|                         $user    = User::getByNickname ($nickname); |             $profile = Profile::getByID($this->trimmed('id')); | ||||||
|                         $profile = $user->getProfile (); |             $profile_id = $profile->getID(); | ||||||
|                         $url     = $profile->profileurl; |         } catch (Exception $e) { | ||||||
|                 } catch (Exception $e) { |             ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||||
|                         ActivityPubReturn::error ('Invalid username.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (!isset ($_GET["page"])) { |  | ||||||
|                         $page = 1; |  | ||||||
|                 } else { |  | ||||||
|                         $page = intval ($this->trimmed ('page')); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if ($page <= 0) { |  | ||||||
|                         ActivityPubReturn::error ('Invalid page number.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 /* Fetch Following */ |  | ||||||
|                 try { |  | ||||||
|                         $since = ($page - 1) * PROFILES_PER_MINILIST; |  | ||||||
|                         $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; |  | ||||||
|                         $sub   = $profile->getSubscribed ($since, $limit); |  | ||||||
|                 } catch (NoResultException $e) { |  | ||||||
|                         ActivityPubReturn::error ('This user is not following anyone.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 /* Calculate total items */ |  | ||||||
|                 $total_subs  = $profile->subscriptionCount(); |  | ||||||
|                 $total_pages = ceil ($total_subs / PROFILES_PER_MINILIST); |  | ||||||
|  |  | ||||||
|                 if ($total_pages == 0) { |  | ||||||
|                         ActivityPubReturn::error ('This user is not following anyone.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if ($page > $total_pages) { |  | ||||||
|                         ActivityPubReturn::error ("There are only {$total_pages} pages."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 /* Get followed' URLs */ |  | ||||||
|                 $subs = array (); |  | ||||||
|                 while ($sub->fetch ()) { |  | ||||||
|                         $subs[] = $sub->profileurl; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $res = [ |  | ||||||
|                   '@context'     => [ |  | ||||||
|                     "https://www.w3.org/ns/activitystreams", |  | ||||||
|                     "https://w3id.org/security/v1", |  | ||||||
|                   ], |  | ||||||
|                   'id'           => "{$url}/following.json", |  | ||||||
|                   'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), |  | ||||||
|                   'totalItems'   => $total_subs, |  | ||||||
|                   'next'         => $page+1 > $total_pages ? null : "{$url}/followers.json?page=".($page+1 == 1 ? 2 : $page+1), |  | ||||||
|                   'prev'         => $page == 1 ? null : "{$url}/followers.json?page=".($page-1 <= 0 ? 1 : $page-1), |  | ||||||
|                   'orderedItems' => $subs |  | ||||||
|                 ]; |  | ||||||
|  |  | ||||||
|                 ActivityPubReturn::answer ($res); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!$profile->isLocal()) { | ||||||
|  |             ActivityPubReturn::error("This is not a local user.", 403); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!isset($_GET["page"])) { | ||||||
|  |             $page = 0; | ||||||
|  |         } else { | ||||||
|  |             $page = intval($this->trimmed('page')); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if ($page < 0) { | ||||||
|  |             ActivityPubReturn::error('Invalid page number.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $since = ($page - 1) * PROFILES_PER_MINILIST; | ||||||
|  |         $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; | ||||||
|  |  | ||||||
|  |         /* Calculate total items */ | ||||||
|  |         $total_subs  = $profile->subscriptionCount(); | ||||||
|  |         $total_pages = ceil($total_subs / PROFILES_PER_MINILIST); | ||||||
|  |  | ||||||
|  |         $res = [ | ||||||
|  |             '@context'     => [ | ||||||
|  |               "https://www.w3.org/ns/activitystreams", | ||||||
|  |               "https://w3id.org/security/v1", | ||||||
|  |             ], | ||||||
|  |             'id'           => common_local_url('apActorFollowing', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), | ||||||
|  |             'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), | ||||||
|  |             'totalItems'   => $total_subs | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         if ($page == 0) { | ||||||
|  |             $res['first'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'?page=1'; | ||||||
|  |         } else { | ||||||
|  |             $res['orderedItems'] = $this->generate_following($profile, $since, $limit); | ||||||
|  |             $res['partOf'] = common_local_url('apActorFollowing', ['id' => $profile_id]); | ||||||
|  |  | ||||||
|  |             if ($page+1 < $total_pages) { | ||||||
|  |                 $res['next'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ($page > 1) { | ||||||
|  |                 $res['prev'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ActivityPubReturn::answer($res); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generates the list of those a given profile is stalking. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Profile $profile | ||||||
|  |      * @param int32 $since | ||||||
|  |      * @param int32 $limit | ||||||
|  |      * @return Array of URIs | ||||||
|  |      */ | ||||||
|  |     public function generate_following($profile, $since, $limit) | ||||||
|  |     { | ||||||
|  |         /* Fetch Following */ | ||||||
|  |         try { | ||||||
|  |             $sub = $profile->getSubscribed($since, $limit); | ||||||
|  |         } catch (NoResultException $e) { | ||||||
|  |             // Just let the exception go on its merry way | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* Get followed' URLs */ | ||||||
|  |         $subs = []; | ||||||
|  |         while ($sub->fetch()) { | ||||||
|  |             $subs[] = ActivityPubPlugin::actor_uri($sub); | ||||||
|  |         } | ||||||
|  |         return $subs; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,115 +0,0 @@ | |||||||
| <?php |  | ||||||
| /** |  | ||||||
|  * GNU social - a federating social network |  | ||||||
|  * |  | ||||||
|  * ActivityPubPlugin implementation for GNU Social |  | ||||||
|  * |  | ||||||
|  * LICENCE: This program is free software: you can redistribute it and/or modify |  | ||||||
|  * it under the terms of the GNU Affero General Public License as published by |  | ||||||
|  * the Free Software Foundation, either version 3 of the License, or |  | ||||||
|  * (at your option) any later version. |  | ||||||
|  * |  | ||||||
|  * This program is distributed in the hope that it will be useful, |  | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|  * GNU Affero General Public License for more details. |  | ||||||
|  * |  | ||||||
|  * You should have received a copy of the GNU Affero General Public License |  | ||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      https://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| if (!defined ('GNUSOCIAL')) { |  | ||||||
|         exit (1); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Actor's Inbox |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      http://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| class apActorInboxAction extends ManagedAction |  | ||||||
| { |  | ||||||
|         protected $needLogin = false; |  | ||||||
|         protected $canPost   = true; |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Handle the Actor Inbox request |  | ||||||
|          * |  | ||||||
|          * @return void |  | ||||||
|          */ |  | ||||||
|         protected function handle () |  | ||||||
|         { |  | ||||||
|                 $nickname  = $this->trimmed ('nickname'); |  | ||||||
|                 try { |  | ||||||
|                         $user    = User::getByNickname ($nickname); |  | ||||||
|                         $profile = $user->getProfile (); |  | ||||||
|                         $url     = $profile->profileurl; |  | ||||||
|                 } catch (Exception $e) { |  | ||||||
|                         ActivityPubReturn::error ("Invalid username."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if ($_SERVER['REQUEST_METHOD'] !== 'POST') { |  | ||||||
|                         ActivityPubReturn::error ("C2S not implemented just yet."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $data = json_decode (file_get_contents ('php://input')); |  | ||||||
|  |  | ||||||
|                 // Validate data |  | ||||||
|                 if (!(isset($data->type))) { |  | ||||||
|                         ActivityPubReturn::error ("Type was not specified."); |  | ||||||
|                 } |  | ||||||
|                 if (!isset($data->actor)) { |  | ||||||
|                         ActivityPubReturn::error ("Actor was not specified."); |  | ||||||
|                 } |  | ||||||
|                 if (!isset($data->object)) { |  | ||||||
|                         ActivityPubReturn::error ("Object was not specified."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // Get valid Actor object |  | ||||||
|                 try { |  | ||||||
|                         require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php"; |  | ||||||
|                         $actor_profile = new Activitypub_explorer; |  | ||||||
|                         $actor_profile = $actor_profile->lookup ($data->actor); |  | ||||||
|                         $actor_profile = $actor_profile[0]; |  | ||||||
|                 } catch (Exception $e) { |  | ||||||
|                         ActivityPubReturn::error ("Invalid Actor.", 404); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $to_profiles = array ($user); |  | ||||||
|  |  | ||||||
|                 // Process request |  | ||||||
|                 switch ($data->type) { |  | ||||||
|                         case "Create": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Create.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Delete": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Delete.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Follow": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Follow.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Like": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Like.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Undo": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Undo.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Announce": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Announce.php"; |  | ||||||
|                                 break; |  | ||||||
|                         default: |  | ||||||
|                                 ActivityPubReturn::error ("Invalid type value."); |  | ||||||
|                 } |  | ||||||
|         } |  | ||||||
| } |  | ||||||
							
								
								
									
										152
									
								
								actions/apactorliked.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										152
									
								
								actions/apactorliked.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,152 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Actor's Liked Collection | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class apActorLikedAction extends ManagedAction | ||||||
|  | { | ||||||
|  |     protected $needLogin = false; | ||||||
|  |     protected $canPost   = true; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handle the Liked Collection request | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     protected function handle() | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $profile = Profile::getByID($this->trimmed('id')); | ||||||
|  |             $profile_id = $profile->getID(); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!$profile->isLocal()) { | ||||||
|  |             ActivityPubReturn::error("This is not a local user.", 403); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $limit    = intval($this->trimmed('limit')); | ||||||
|  |         $since_id = intval($this->trimmed('since_id')); | ||||||
|  |         $max_id   = intval($this->trimmed('max_id')); | ||||||
|  |  | ||||||
|  |         $limit    = empty($limit) ? 40 : $limit;       // Default is 40 | ||||||
|  |         $since_id = empty($since_id) ? null : $since_id; | ||||||
|  |         $max_id   = empty($max_id) ? null : $max_id; | ||||||
|  |  | ||||||
|  |         // Max is 80 | ||||||
|  |         if ($limit > 80) { | ||||||
|  |             $limit = 80; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $fave = $this->fetch_faves($profile_id, $limit, $since_id, $max_id); | ||||||
|  |  | ||||||
|  |         $faves = array(); | ||||||
|  |         while ($fave->fetch()) { | ||||||
|  |             $faves[] = $this->pretty_fave(clone ($fave)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $res = [ | ||||||
|  |             '@context'     => [ | ||||||
|  |               "https://www.w3.org/ns/activitystreams", | ||||||
|  |               "https://w3id.org/security/v1", | ||||||
|  |             ], | ||||||
|  |             'id'           => common_local_url('apActorLiked', ['id' => $profile_id]), | ||||||
|  |             'type'         => 'OrderedCollection', | ||||||
|  |             'totalItems'   => Fave::countByProfile($profile), | ||||||
|  |             'orderedItems' => $faves | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         ActivityPubReturn::answer($res); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Take a fave object and turns it in a pretty array to be used | ||||||
|  |      * as a plugin answer | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Fave $fave_object | ||||||
|  |      * @return array pretty array representating a Fave | ||||||
|  |      */ | ||||||
|  |     protected function pretty_fave($fave_object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             'created' => $fave_object->created, | ||||||
|  |             'object' => Activitypub_notice::notice_to_array(Notice::getByID($fave_object->notice_id)) | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Fetch faves | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param int32 $user_id | ||||||
|  |      * @param int32 $limit | ||||||
|  |      * @param int32 $since_id | ||||||
|  |      * @param int32 $max_id | ||||||
|  |      * @return Fave fetchable fave collection | ||||||
|  |      */ | ||||||
|  |     private static function fetch_faves( | ||||||
|  |             $user_id, | ||||||
|  |             $limit = 40, | ||||||
|  |             $since_id = null, | ||||||
|  |             $max_id = null | ||||||
|  |         ) { | ||||||
|  |         $fav = new Fave(); | ||||||
|  |  | ||||||
|  |         $fav->user_id = $user_id; | ||||||
|  |  | ||||||
|  |         $fav->orderBy('modified DESC'); | ||||||
|  |  | ||||||
|  |         if ($since_id != null) { | ||||||
|  |             $fav->whereAdd("notice_id  > {$since_id}"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if ($max_id != null) { | ||||||
|  |             $fav->whereAdd("notice_id  < {$max_id}"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $fav->limit($limit); | ||||||
|  |  | ||||||
|  |         $fav->find(); | ||||||
|  |  | ||||||
|  |         return $fav; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,146 +0,0 @@ | |||||||
| <?php |  | ||||||
| /** |  | ||||||
|  * GNU social - a federating social network |  | ||||||
|  * |  | ||||||
|  * ActivityPubPlugin implementation for GNU Social |  | ||||||
|  * |  | ||||||
|  * LICENCE: This program is free software: you can redistribute it and/or modify |  | ||||||
|  * it under the terms of the GNU Affero General Public License as published by |  | ||||||
|  * the Free Software Foundation, either version 3 of the License, or |  | ||||||
|  * (at your option) any later version. |  | ||||||
|  * |  | ||||||
|  * This program is distributed in the hope that it will be useful, |  | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|  * GNU Affero General Public License for more details. |  | ||||||
|  * |  | ||||||
|  * You should have received a copy of the GNU Affero General Public License |  | ||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      https://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| if (!defined ('GNUSOCIAL')) { |  | ||||||
|         exit (1); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Actor's Liked Collection |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      http://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| class apActorLikedCollectionAction extends ManagedAction |  | ||||||
| { |  | ||||||
|         protected $needLogin = false; |  | ||||||
|         protected $canPost   = true; |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Handle the Liked Collection request |  | ||||||
|          * |  | ||||||
|          * @return void |  | ||||||
|          */ |  | ||||||
|         protected function handle () |  | ||||||
|         { |  | ||||||
|                 $nickname = $this->trimmed ('nickname'); |  | ||||||
|                 try { |  | ||||||
|                         $user    = User::getByNickname ($nickname); |  | ||||||
|                         $profile = $user->getProfile (); |  | ||||||
|                         $url     = $profile->profileurl; |  | ||||||
|                 } catch (Exception $e) { |  | ||||||
|                         ActivityPubReturn::error ('Invalid username.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $limit    = intval ($this->trimmed ('limit')); |  | ||||||
|                 $since_id = intval ($this->trimmed ('since_id')); |  | ||||||
|                 $max_id   = intval ($this->trimmed ('max_id')); |  | ||||||
|  |  | ||||||
|                 $limit    = empty ($limit) ? 40 : $limit;       // Default is 40 |  | ||||||
|                 $since_id = empty ($since_id) ? null : $since_id; |  | ||||||
|                 $max_id   = empty ($max_id) ? null : $max_id; |  | ||||||
|  |  | ||||||
|                 // Max is 80 |  | ||||||
|                 if ($limit > 80) { |  | ||||||
|                         $limit = 80; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $fave = $this->fetch_faves($user->getID(), $limit, $since_id, $max_id); |  | ||||||
|  |  | ||||||
|                 $faves = array(); |  | ||||||
|                 while ($fave->fetch ()) { |  | ||||||
|                         $faves[] = $this->pretty_fave (clone ($fave)); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $res = [ |  | ||||||
|                   '@context'          => [ |  | ||||||
|                     "https://www.w3.org/ns/activitystreams", |  | ||||||
|                     [ |  | ||||||
|                       "@language" => "en" |  | ||||||
|                     ] |  | ||||||
|                   ], |  | ||||||
|                   'id'           => "{$url}/liked.json", |  | ||||||
|                   'type'         => 'OrderedCollection', |  | ||||||
|                   'totalItems'   => Fave::countByProfile ($profile), |  | ||||||
|                   'orderedItems' => $faves |  | ||||||
|                 ]; |  | ||||||
|  |  | ||||||
|                 ActivityPubReturn::answer ($res); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Take a fave object and turns it in a pretty array to be used |  | ||||||
|          * as a plugin answer |  | ||||||
|          * |  | ||||||
|          * @param Fave $fave_object |  | ||||||
|          * @return array pretty array representating a Fave |  | ||||||
|          */ |  | ||||||
|         protected function pretty_fave ($fave_object) |  | ||||||
|         { |  | ||||||
|                 $res = array("uri" => $fave_object->uri, |  | ||||||
|                              "created" => $fave_object->created, |  | ||||||
|                              "object" => Activitypub_notice::notice_to_array (Notice::getByID ($fave_object->notice_id))); |  | ||||||
|  |  | ||||||
|                 return $res; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Fetch faves |  | ||||||
|          * |  | ||||||
|          * @param int32 $user_id |  | ||||||
|          * @param int32 $limit |  | ||||||
|          * @param int32 $since_id |  | ||||||
|          * @param int32 $max_id |  | ||||||
|          * @return Fave fetchable fave collection |  | ||||||
|          */ |  | ||||||
|         private static function fetch_faves ($user_id, $limit = 40, $since_id = null, |  | ||||||
|                                              $max_id = null) |  | ||||||
|         { |  | ||||||
|                 $fav = new Fave (); |  | ||||||
|  |  | ||||||
|                 $fav->user_id = $user_id; |  | ||||||
|  |  | ||||||
|                 $fav->orderBy ('modified DESC'); |  | ||||||
|  |  | ||||||
|                 if ($since_id != null) { |  | ||||||
|                         $fav->whereAdd ("notice_id  > {$since_id}"); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if ($max_id != null) { |  | ||||||
|                         $fav->whereAdd ("notice_id  < {$max_id}"); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $fav->limit ($limit); |  | ||||||
|  |  | ||||||
|                 $fav->find (); |  | ||||||
|  |  | ||||||
|                 return $fav; |  | ||||||
|         } |  | ||||||
| } |  | ||||||
							
								
								
									
										134
									
								
								actions/apactoroutbox.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								actions/apactoroutbox.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Inbox Request Handler | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class apActorOutboxAction extends ManagedAction | ||||||
|  | { | ||||||
|  |     protected $needLogin = false; | ||||||
|  |     protected $canPost   = true; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handle the Outbox request | ||||||
|  |      * | ||||||
|  |      * @author Daniel Supernault <danielsupernault@gmail.com> | ||||||
|  |      */ | ||||||
|  |     protected function handle() | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $profile = Profile::getByID($this->trimmed('id')); | ||||||
|  |             $profile_id = $profile->getID(); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!$profile->isLocal()) { | ||||||
|  |             ActivityPubReturn::error("This is not a local user.", 403); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!isset($_GET["page"])) { | ||||||
|  |             $page = 0; | ||||||
|  |         } else { | ||||||
|  |             $page = intval($this->trimmed('page')); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if ($page < 0) { | ||||||
|  |             ActivityPubReturn::error('Invalid page number.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $since = ($page - 1) * PROFILES_PER_MINILIST; | ||||||
|  |         $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; | ||||||
|  |  | ||||||
|  |         /* Calculate total items */ | ||||||
|  |         $total_notes = $profile->noticeCount(); | ||||||
|  |         $total_pages = ceil($total_notes / PROFILES_PER_MINILIST); | ||||||
|  |  | ||||||
|  |         $res = [ | ||||||
|  |             '@context'     => [ | ||||||
|  |               "https://www.w3.org/ns/activitystreams", | ||||||
|  |               "https://w3id.org/security/v1", | ||||||
|  |             ], | ||||||
|  |             'id'           => common_local_url('apActorOutbox', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), | ||||||
|  |             'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), | ||||||
|  |             'totalItems'   => $total_notes | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         if ($page == 0) { | ||||||
|  |             $res['first'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'?page=1'; | ||||||
|  |         } else { | ||||||
|  |             $res['orderedItems'] = $this->generate_outbox($profile); | ||||||
|  |             $res['partOf'] = common_local_url('apActorOutbox', ['id' => $profile_id]); | ||||||
|  |  | ||||||
|  |             if ($page+1 < $total_pages) { | ||||||
|  |                 $res['next'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if ($page > 1) { | ||||||
|  |                 $res['prev'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ActivityPubReturn::answer($res); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generates a list of people following given profile. | ||||||
|  |      * | ||||||
|  |      * @author Daniel Supernault <danielsupernault@gmail.com> | ||||||
|  |      * @param Profile $profile | ||||||
|  |      * @return Array of Notices | ||||||
|  |      */ | ||||||
|  |     public function generate_outbox($profile) | ||||||
|  |     { | ||||||
|  |         /* Fetch Notices */ | ||||||
|  |         $notices = []; | ||||||
|  |         $notice = $profile->getNotices(); | ||||||
|  |         while ($notice->fetch()) { | ||||||
|  |             $note = $notice; | ||||||
|  |  | ||||||
|  |             // TODO: Handle other types | ||||||
|  |             if ($note->object_type == 'http://activitystrea.ms/schema/1.0/note') { | ||||||
|  |                 $notices[] = Activitypub_create::create_to_array( | ||||||
|  |                     ActivityPubPlugin::actor_uri($note->getProfile()), | ||||||
|  |                     Activitypub_notice::notice_to_array($note) | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $notices; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								actions/apactorprofile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										59
									
								
								actions/apactorprofile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -19,14 +19,13 @@ | |||||||
|  * |  * | ||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -34,34 +33,44 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  * |  * | ||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      http://www.gnu.org/software/social/ |  * @link      http://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| class apActorProfileAction extends ManagedAction | class apActorProfileAction extends ManagedAction | ||||||
| { | { | ||||||
|         protected $needLogin = false; |     protected $needLogin = false; | ||||||
|         protected $canPost   = true; |     protected $canPost   = true; | ||||||
|  |  | ||||||
|         /** |     /** | ||||||
|          * Handle the Actor Profile request |      * Handle the Actor Profile request | ||||||
|          * |      * | ||||||
|          * @return void |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          */ |      * @return void | ||||||
|         protected function handle() |      */ | ||||||
|         { |     protected function handle() | ||||||
|                 $nickname = $this->trimmed ('nickname'); |     { | ||||||
|                 try { |         if (!empty($id = $this->trimmed('id'))) { | ||||||
|                         $user    = User::getByNickname ($nickname); |             try { | ||||||
|                         $profile = $user->getProfile (); |                 $profile = Profile::getByID($id); | ||||||
|                 } |             } catch (Exception $e) { | ||||||
|                 catch (Exception $e) { |                 ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||||
|                         ActivityPubReturn::error ('Invalid username.', 404); |             } | ||||||
|                 } |             unset($id); | ||||||
|  |         } else { | ||||||
|                 $res = Activitypub_profile::profile_to_array ($profile); |             try { | ||||||
|  |                 $profile = User::getByNickname($this->trimmed('nickname'))->getProfile(); | ||||||
|                 ActivityPubReturn::answer ($res); |             } catch (Exception $e) { | ||||||
|  |                 ActivityPubReturn::error('Invalid username.', 404); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!$profile->isLocal()) { | ||||||
|  |             ActivityPubReturn::error("This is not a local user.", 403); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $res = Activitypub_profile::profile_to_array($profile); | ||||||
|  |  | ||||||
|  |         ActivityPubReturn::answer($res); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										103
									
								
								actions/apinbox.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										103
									
								
								actions/apinbox.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Inbox Request Handler | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class apInboxAction extends ManagedAction | ||||||
|  | { | ||||||
|  |     protected $needLogin = false; | ||||||
|  |     protected $canPost   = true; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handle the Inbox request | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     protected function handle() | ||||||
|  |     { | ||||||
|  |         if ($_SERVER['REQUEST_METHOD'] !== 'POST') { | ||||||
|  |             ActivityPubReturn::error('Only POST requests allowed.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         common_debug('ActivityPub Inbox: Received a POST request.'); | ||||||
|  |         $data = file_get_contents('php://input'); | ||||||
|  |         common_debug('ActivityPub Inbox: Request contents: '.$data); | ||||||
|  |         $data = json_decode(file_get_contents('php://input'), true); | ||||||
|  |  | ||||||
|  |         if (!isset($data['actor'])) { | ||||||
|  |             ActivityPubReturn::error('Actor not found in the request.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $actor = ActivityPub_explorer::get_profile_from_url($data['actor']); | ||||||
|  |         $actor_public_key = new Activitypub_rsa(); | ||||||
|  |         $actor_public_key = $actor_public_key->ensure_public_key($actor); | ||||||
|  |  | ||||||
|  |         common_debug('ActivityPub Inbox: HTTP Signature: Validation will now start!'); | ||||||
|  |  | ||||||
|  |         $headers = $this->get_all_headers(); | ||||||
|  |         common_debug('ActivityPub Inbox: Request Headers: '.print_r($headers, true)); | ||||||
|  |  | ||||||
|  |         // TODO: Validate HTTP Signature, if it fails, attempt once with profile update | ||||||
|  |  | ||||||
|  |         common_debug('ActivityPub Inbox: HTTP Signature: Authorized request. Will now start the inbox handler.'); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             new Activitypub_inbox_handler($data, $actor); | ||||||
|  |             ActivityPubReturn::answer(); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             ActivityPubReturn::error($e->getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Get all HTTP header key/values as an associative array for the current request. | ||||||
|  |      * | ||||||
|  |      * @author PHP Manual Contributed Notes <joyview@gmail.com> | ||||||
|  |      * @return string[string] The HTTP header key/value pairs. | ||||||
|  |      */ | ||||||
|  |     private function get_all_headers() | ||||||
|  |     { | ||||||
|  |         $headers = []; | ||||||
|  |         foreach ($_SERVER as $name => $value) { | ||||||
|  |             if (substr($name, 0, 5) == 'HTTP_') { | ||||||
|  |                 $headers[strtolower(str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))))] = $value; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $headers; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								actions/inbox/Follow.php → actions/apnotice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										63
									
								
								actions/inbox/Follow.php → actions/apnotice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,39 +20,48 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Validate Object
 | /** | ||||||
| if (!is_string ($data->object)) { |  * Notice (Local notices only) | ||||||
|         ActivityPubReturn::error ("Invalid Object object, URL expected."); |  * | ||||||
| } |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class apNoticeAction extends ManagedAction | ||||||
|  | { | ||||||
|  |     protected $needLogin = false; | ||||||
|  |     protected $canPost   = true; | ||||||
| 
 | 
 | ||||||
| // Get valid Object profile
 |     /** | ||||||
| try { |      * Handle the Notice request | ||||||
|         $object_profile = new Activitypub_explorer; |      * | ||||||
|         $object_profile = $object_profile->lookup ($data->object)[0]; |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
| } catch(Exception $e) { |      * @return void | ||||||
|         ActivityPubReturn::error ("Invalid Object Actor URL.", 404); |      */ | ||||||
| } |     protected function handle() | ||||||
| 
 |     { | ||||||
| try { |         try { | ||||||
|         if (!Subscription::exists ($actor_profile, $object_profile)) { |             $notice = Notice::getByID($this->trimmed('id')); | ||||||
|                 Subscription::start ($actor_profile, $object_profile); |         } catch (Exception $e) { | ||||||
|                 $res = array ("@context" => "https://www.w3.org/ns/activitystreams", |             ActivityPubReturn::error('Invalid Notice URI.', 404); | ||||||
|                           "type"   => "Follow", |  | ||||||
|                           "actor"  => $data->actor, |  | ||||||
|                           "object" => $data->object); |  | ||||||
|                 ActivityPubReturn::answer ($res); |  | ||||||
|         } else { |  | ||||||
|                 ActivityPubReturn::error ("Already following.", 409); |  | ||||||
|         } |         } | ||||||
| } catch (Exception $e) { | 
 | ||||||
|         ActivityPubReturn::error ("Invalid Object Actor URL.", 404); |         if (!$notice->isLocal()) { | ||||||
|  |             ActivityPubReturn::error("This is not a local notice.", 403); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $res = Activitypub_notice::notice_to_array($notice); | ||||||
|  | 
 | ||||||
|  |         ActivityPubReturn::answer($res); | ||||||
|  |     } | ||||||
| } | } | ||||||
| @@ -1,135 +0,0 @@ | |||||||
| <?php |  | ||||||
| require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php"; |  | ||||||
| /** |  | ||||||
|  * GNU social - a federating social network |  | ||||||
|  * |  | ||||||
|  * ActivityPubPlugin implementation for GNU Social |  | ||||||
|  * |  | ||||||
|  * LICENCE: This program is free software: you can redistribute it and/or modify |  | ||||||
|  * it under the terms of the GNU Affero General Public License as published by |  | ||||||
|  * the Free Software Foundation, either version 3 of the License, or |  | ||||||
|  * (at your option) any later version. |  | ||||||
|  * |  | ||||||
|  * This program is distributed in the hope that it will be useful, |  | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|  * GNU Affero General Public License for more details. |  | ||||||
|  * |  | ||||||
|  * You should have received a copy of the GNU Affero General Public License |  | ||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      https://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| if (!defined ('GNUSOCIAL')) { |  | ||||||
|         exit (1); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Shared Inbox Handler |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      http://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| class apSharedInboxAction extends ManagedAction |  | ||||||
| { |  | ||||||
|         protected $needLogin = false; |  | ||||||
|         protected $canPost   = true; |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Handle the Shared Inbox request |  | ||||||
|          * |  | ||||||
|          * @return void |  | ||||||
|          */ |  | ||||||
|         protected function handle () |  | ||||||
|         { |  | ||||||
|                 if ($_SERVER['REQUEST_METHOD'] !== 'POST') { |  | ||||||
|                         ActivityPubReturn::error ("Only POST requests allowed."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $data = json_decode (file_get_contents ('php://input')); |  | ||||||
|  |  | ||||||
|                 // Validate data |  | ||||||
|                 if (!isset($data->type)) { |  | ||||||
|                         ActivityPubReturn::error ("Type was not specified."); |  | ||||||
|                 } |  | ||||||
|                 if (!isset($data->actor)) { |  | ||||||
|                         ActivityPubReturn::error ("Actor was not specified."); |  | ||||||
|                 } |  | ||||||
|                 if (!isset($data->object)) { |  | ||||||
|                         ActivityPubReturn::error ("Object was not specified."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $discovery = new Activitypub_explorer; |  | ||||||
|  |  | ||||||
|                 // Get valid Actor object |  | ||||||
|                 try { |  | ||||||
|                         $actor_profile = $discovery->lookup ($data->actor); |  | ||||||
|                         $actor_profile = $actor_profile[0]; |  | ||||||
|                 } catch (Exception $e) { |  | ||||||
|                         ActivityPubReturn::error ("Invalid Actor.", 404); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 unset ($discovery); |  | ||||||
|  |  | ||||||
|                 // Public To: |  | ||||||
|                 $public_to = array ("https://www.w3.org/ns/activitystreams#Public", |  | ||||||
|                                     "Public", |  | ||||||
|                                     "as:Public"); |  | ||||||
|  |  | ||||||
|                 // Process request |  | ||||||
|                 switch ($data->type) { |  | ||||||
|                         case "Create": |  | ||||||
|                                 if (!isset($data->to)) { |  | ||||||
|                                         ActivityPubReturn::error ("To was not specified."); |  | ||||||
|                                 } |  | ||||||
|                                 $discovery = new Activitypub_Discovery; |  | ||||||
|                                 $to_profiles = array (); |  | ||||||
|                                 // Generate To objects |  | ||||||
|                                 if (is_array ($data->to)) { |  | ||||||
|                                         // Remove duplicates from To actors set |  | ||||||
|                                         array_unique ($data->to); |  | ||||||
|                                         foreach ($data->to as $to_url) { |  | ||||||
|                                                 try { |  | ||||||
|                                                         $to_profiles = array_merge ($to_profiles, $discovery->lookup ($to_url)); |  | ||||||
|                                                 } catch (Exception $e) { |  | ||||||
|                                                         // XXX: Invalid actor found, not sure how we handle those |  | ||||||
|                                                 } |  | ||||||
|                                         } |  | ||||||
|                                 } else if (empty ($data->to) || in_array ($data->to, $public_to)) { |  | ||||||
|                                         // No need to do anything else at this point, let's just break out the if |  | ||||||
|                                 } else { |  | ||||||
|                                         try { |  | ||||||
|                                                 $to_profiles[]= $discovery->lookup ($data->to); |  | ||||||
|                                         } catch (Exception $e) { |  | ||||||
|                                                 ActivityPubReturn::error ("Invalid Actor.", 404); |  | ||||||
|                                         } |  | ||||||
|                                 } |  | ||||||
|                                 unset ($discovery); |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Create.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Follow": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Follow.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Like": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Like.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Announce": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Announce.php"; |  | ||||||
|                                 break; |  | ||||||
|                         case "Undo": |  | ||||||
|                                 require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Undo.php"; |  | ||||||
|                                 break; |  | ||||||
|                         default: |  | ||||||
|                                 ActivityPubReturn::error ("Invalid type value."); |  | ||||||
|                 } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,93 +0,0 @@ | |||||||
| <?php |  | ||||||
| /** |  | ||||||
|  * GNU social - a federating social network |  | ||||||
|  * |  | ||||||
|  * ActivityPubPlugin implementation for GNU Social |  | ||||||
|  * |  | ||||||
|  * LICENCE: This program is free software: you can redistribute it and/or modify |  | ||||||
|  * it under the terms of the GNU Affero General Public License as published by |  | ||||||
|  * the Free Software Foundation, either version 3 of the License, or |  | ||||||
|  * (at your option) any later version. |  | ||||||
|  * |  | ||||||
|  * This program is distributed in the hope that it will be useful, |  | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|  * GNU Affero General Public License for more details. |  | ||||||
|  * |  | ||||||
|  * You should have received a copy of the GNU Affero General Public License |  | ||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      https://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| if (!defined ('GNUSOCIAL')) { |  | ||||||
|         exit (1); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $valid_object_types = array ("Note"); |  | ||||||
|  |  | ||||||
| // Validate data |  | ||||||
| if (!(isset ($data->object->type) && in_array ($data->object->type, $valid_object_types))) { |  | ||||||
|         ActivityPubReturn::error ("Invalid Object type."); |  | ||||||
| } |  | ||||||
| if (!isset ($data->object->content)) { |  | ||||||
|         ActivityPubReturn::error ("Object content was not specified."); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $content = $data->object->content; |  | ||||||
|  |  | ||||||
| $act = new Activity (); |  | ||||||
| $act->verb = ActivityVerb::POST; |  | ||||||
| $act->time = time (); |  | ||||||
| $act->actor = $actor_profile->asActivityObject (); |  | ||||||
|  |  | ||||||
| $act->context = new ActivityContext (); |  | ||||||
|  |  | ||||||
| // Is this a reply? |  | ||||||
| if (isset ($data->object->reply_to)) { |  | ||||||
|         $reply_to = Notice::getByUri ($data->object->reply_to); |  | ||||||
|         $act->context->replyToID = $reply_to->getUri (); |  | ||||||
|         $act->context->replyToUrl = $data->object->reply_to; |  | ||||||
| } else { |  | ||||||
|         $reply_to = null; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $act->context->attention = common_get_attentions ($content, $actor_profile, $reply_to); |  | ||||||
|  |  | ||||||
| foreach ($to_profiles as $to) |  | ||||||
| { |  | ||||||
|         $act->context->attention[$to->getUri ()] = "http://activitystrea.ms/schema/1.0/person"; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Reject notice if it is too long (without the HTML) |  | ||||||
| // This is done after MediaFile::fromUpload etc. just to act the same as the ApiStatusesUpdateAction |  | ||||||
| if (Notice::contentTooLong ($content)) { |  | ||||||
|         ActivityPubReturn::error ("That's too long. Maximum notice size is %d character."); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $options = array ('source' => 'ActivityPub', 'uri' => $data->id); |  | ||||||
| // $options gets filled with possible scoping settings |  | ||||||
| ToSelector::fillActivity ($this, $act, $options); |  | ||||||
|  |  | ||||||
| $actobj = new ActivityObject (); |  | ||||||
| $actobj->type = ActivityObject::NOTE; |  | ||||||
| $actobj->content = common_render_content ($content, $actor_profile, $reply_to); |  | ||||||
|  |  | ||||||
| // Finally add the activity object to our activity |  | ||||||
| $act->objects[] = $actobj; |  | ||||||
|  |  | ||||||
| try { |  | ||||||
|         $res = array ("@context" => "https://www.w3.org/ns/activitystreams", |  | ||||||
|                   "id"     => $data->id, |  | ||||||
|                   "type"   => "Create", |  | ||||||
|                   "actor"  => $data->actor, |  | ||||||
|                   "object" => Activitypub_notice::notice_to_array (Notice::saveActivity ($act, $actor_profile, $options))); |  | ||||||
|         ActivityPubReturn::answer ($res); |  | ||||||
| } catch (Exception $e) { |  | ||||||
|         ActivityPubReturn::error ($e->getMessage ()); |  | ||||||
| } |  | ||||||
| @@ -1,78 +0,0 @@ | |||||||
| <?php |  | ||||||
| /** |  | ||||||
|  * GNU social - a federating social network |  | ||||||
|  * |  | ||||||
|  * ActivityPubPlugin implementation for GNU Social |  | ||||||
|  * |  | ||||||
|  * LICENCE: This program is free software: you can redistribute it and/or modify |  | ||||||
|  * it under the terms of the GNU Affero General Public License as published by |  | ||||||
|  * the Free Software Foundation, either version 3 of the License, or |  | ||||||
|  * (at your option) any later version. |  | ||||||
|  * |  | ||||||
|  * This program is distributed in the hope that it will be useful, |  | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|  * GNU Affero General Public License for more details. |  | ||||||
|  * |  | ||||||
|  * You should have received a copy of the GNU Affero General Public License |  | ||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  * |  | ||||||
|  * @category  Plugin |  | ||||||
|  * @package   GNUsocial |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  | ||||||
|  * @link      https://www.gnu.org/software/social/ |  | ||||||
|  */ |  | ||||||
| if (!defined ('GNUSOCIAL')) { |  | ||||||
|         exit (1); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Validate data |  | ||||||
| if (!isset ($data->type)) { |  | ||||||
|         ActivityPubReturn::error ("Type was not specified."); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| switch ($data->object->type) { |  | ||||||
| case "Like": |  | ||||||
|         try { |  | ||||||
|                 // Validate data |  | ||||||
|                 if (!isset ($data->object->object)) { |  | ||||||
|                         ActivityPubReturn::error ("Object Notice URL was not specified."); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 Fave::removeEntry ($actor_profile, Notice::getByUri ($data->object->object)); |  | ||||||
|                 ActivityPubReturn::answer ("Notice disfavorited successfully."); |  | ||||||
|         } catch (Exception $e) { |  | ||||||
|                 ActivityPubReturn::error ($e->getMessage (), 403); |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
| case "Follow": |  | ||||||
|         // Validate data |  | ||||||
|         if (!isset ($data->object->object)) { |  | ||||||
|                 ActivityPubReturn::error ("Object Actor URL was not specified."); |  | ||||||
|         } |  | ||||||
|         // Get valid Object profile |  | ||||||
|         try { |  | ||||||
|                 $object_profile = new Activitypub_explorer; |  | ||||||
|                 $object_profile = $object_profile->lookup ($data->object->object)[0]; |  | ||||||
|         } catch (Exception $e) { |  | ||||||
|                 ActivityPubReturn::error ("Invalid Object Actor URL.", 404); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|                 if (Subscription::exists ($actor_profile, $object_profile)) { |  | ||||||
|                         Subscription::cancel ($actor_profile, $object_profile); |  | ||||||
|                         ActivityPubReturn::answer ("You are no longer following this person."); |  | ||||||
|                 } else { |  | ||||||
|                         ActivityPubReturn::error ("You are not following this person already.", 409); |  | ||||||
|                 } |  | ||||||
|         } catch (Exception $e) { |  | ||||||
|                 ActivityPubReturn::error ("Invalid Object Actor URL.", 404); |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
| default: |  | ||||||
|         ActivityPubReturn::error ("Invalid object type."); |  | ||||||
|         break; |  | ||||||
| } |  | ||||||
							
								
								
									
										88
									
								
								classes/Activitypub_accept.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										88
									
								
								classes/Activitypub_accept.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub error representation | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_accept extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Accept | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $object | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function accept_to_array($object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             'id'     => common_root_url().'accept_follow_from_'.urlencode($object['actor']).'_to_'.urlencode($object['object']), | ||||||
|  |             'type'   => 'Accept', | ||||||
|  |             'actor'  => $object['object'], | ||||||
|  |             'object' => $object | ||||||
|  |         ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Verifies if a given object is acceptable for an Accept Activity. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Array $object | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public static function validate_object($object) | ||||||
|  |     { | ||||||
|  |         if (!is_array($object)) { | ||||||
|  |             throw new Exception('Invalid Object Format for Accept Activity.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['type'])) { | ||||||
|  |             throw new Exception('Object type was not specified for Accept Activity.'); | ||||||
|  |         } | ||||||
|  |         switch ($object['type']) { | ||||||
|  |             case 'Follow': | ||||||
|  |                 // Validate data | ||||||
|  |                 if (!filter_var($object['object'], FILTER_VALIDATE_URL)) { | ||||||
|  |                     throw new Exception("Object is not a valid Object URI for Activity."); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new Exception('This is not a supported Object Type for Accept Activity.'); | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								actions/inbox/Delete.php → classes/Activitypub_announce.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										42
									
								
								actions/inbox/Delete.php → classes/Activitypub_announce.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,22 +20,40 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| try { | /** | ||||||
|         Activitypub_notice::getByUri ($data->object)->deleteAs ($actor_profile); |  * ActivityPub error representation | ||||||
|         $res = array ("@context" => "https://www.w3.org/ns/activitystreams", |  * | ||||||
|                   "type"   => "Delete", |  * @category  Plugin | ||||||
|                   "actor"  => $data->actor, |  * @package   GNUsocial | ||||||
|                   "object" => $data->object); |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|         ActivityPubReturn::answer ($res); |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
| } catch (Exception $e) { |  * @link      http://www.gnu.org/software/social/ | ||||||
|         ActivityPubReturn::error ($e->getMessage (), 403); |  */ | ||||||
|  | class Activitypub_announce extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Announce | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $object | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function announce_to_array($actor, $object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             "type"   => "Announce", | ||||||
|  |             "actor"  => $actor, | ||||||
|  |             "object" => $object | ||||||
|  |         ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
| } | } | ||||||
							
								
								
									
										63
									
								
								classes/Activitypub_attachment.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										63
									
								
								classes/Activitypub_attachment.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,13 +20,12 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -40,38 +39,32 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  */ |  */ | ||||||
| class Activitypub_attachment extends Managed_DataObject | class Activitypub_attachment extends Managed_DataObject | ||||||
| { | { | ||||||
|         /** |     /** | ||||||
|          * Generates a pretty array from an Attachment object |      * Generates a pretty array from an Attachment object | ||||||
|          * |      * | ||||||
|          * @param Attachment $attachment |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          * @return pretty array to be used in a response |      * @param Attachment $attachment | ||||||
|          */ |      * @return pretty array to be used in a response | ||||||
|         public static function attachment_to_array ($attachment) |      */ | ||||||
|         { |     public static function attachment_to_array($attachment) | ||||||
|                 $res = [ |     { | ||||||
|                         '@context'  => [ |         $res = [ | ||||||
|                                 "https://www.w3.org/ns/activitystreams", |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|                                 [ |             'type'      => 'Document', | ||||||
|                                         "@language" => "en" |             'mediaType' => $attachment->mimetype, | ||||||
|                                 ] |             'url'       => $attachment->getUrl(), | ||||||
|                         ], |             'size'      => intval($attachment->size), // $attachment->getSize () | ||||||
|                         'id'       => $attachment->getID (), |             'name'      => $attachment->getTitle(), | ||||||
|                         'mimetype' => $attachment->mimetype, |         ]; | ||||||
|                         'url'      => $attachment->getUrl (), |  | ||||||
|                         'size'     => intval($attachment->size), // $attachment->getSize () |  | ||||||
|                         'title'    => $attachment->getTitle (), |  | ||||||
|                         'meta'     => null |  | ||||||
|                 ]; |  | ||||||
|  |  | ||||||
|                 // Image |         // Image | ||||||
|                 if (substr ($res["mimetype"], 0, 5) == "image") |         if (substr($res["mediaType"], 0, 5) == "image") { | ||||||
|                 { |             $res["meta"]= [ | ||||||
|                         $res["meta"]= [ |                 'width'  => $attachment->width, | ||||||
|                                 'width'  => $attachment->width, |                 'height' => $attachment->height | ||||||
|                                 'height' => $attachment->height |             ]; | ||||||
|                         ]; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return $res; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										88
									
								
								classes/Activitypub_create.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										88
									
								
								classes/Activitypub_create.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub error representation | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_create extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Create | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $actor | ||||||
|  |      * @param array $object | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function create_to_array($actor, $object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             'id'     => $object['id'].'/create', | ||||||
|  |             'type'   => 'Create', | ||||||
|  |             'to'     => $object['to'], | ||||||
|  |             'cc'     => $object['cc'], | ||||||
|  |             'actor'  => $actor, | ||||||
|  |             'object' => $object | ||||||
|  |         ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Verifies if a given object is acceptable for a Create Activity. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Array $object | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public static function validate_object($object) | ||||||
|  |     { | ||||||
|  |         if (!is_array($object)) { | ||||||
|  |             throw new Exception('Invalid Object Format for Create Activity.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['type'])) { | ||||||
|  |             throw new Exception('Object type was not specified for Create Activity.'); | ||||||
|  |         } | ||||||
|  |         switch ($object['type']) { | ||||||
|  |             case 'Note': | ||||||
|  |                 // Validate data | ||||||
|  |                 Activitypub_notice::validate_note($object); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new Exception('This is not a supported Object Type for Create Activity.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								actions/inbox/Announce.php → classes/Activitypub_delete.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										39
									
								
								actions/inbox/Announce.php → classes/Activitypub_delete.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,18 +20,41 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| try { | /** | ||||||
|         Notice::getByUri ($data->object)->repeat ($actor_profile, "ActivityPub"); |  * ActivityPub error representation | ||||||
|         ActivityPubReturn::answer ("Notice repeated successfully."); |  * | ||||||
| } catch (Exception $e) { |  * @category  Plugin | ||||||
|         ActivityPubReturn::error ($e->getMessage (), 403); |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_delete extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Delete | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $object | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function delete_to_array($actor, $object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             'id'     => $object.'/delete', | ||||||
|  |             'type'   => 'Delete', | ||||||
|  |             'actor'  => $actor, | ||||||
|  |             'object' => $object | ||||||
|  |         ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
| } | } | ||||||
							
								
								
									
										32
									
								
								classes/Activitypub_error.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										32
									
								
								classes/Activitypub_error.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,13 +20,12 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -40,17 +39,18 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  */ |  */ | ||||||
| class Activitypub_error extends Managed_DataObject | class Activitypub_error extends Managed_DataObject | ||||||
| { | { | ||||||
|         /** |     /** | ||||||
|          * Generates a pretty error from a string |      * Generates a pretty error from a string | ||||||
|          * |      * | ||||||
|          * @param string $m |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          * @return pretty array to be used in a response |      * @param string $m | ||||||
|          */ |      * @return pretty array to be used in a response | ||||||
|         public static function error_message_to_array ($m) |      */ | ||||||
|         { |     public static function error_message_to_array($m) | ||||||
|                 $res = [ |     { | ||||||
|                         'error'=> $m |         $res = [ | ||||||
|                 ]; |             'error'=> $m | ||||||
|                 return $res; |         ]; | ||||||
|         } |         return $res; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										91
									
								
								classes/Activitypub_follow.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										91
									
								
								classes/Activitypub_follow.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub error representation | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_follow extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a subscription | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $actor | ||||||
|  |      * @param string $object | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function follow_to_array($actor, $object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             'id'     => common_root_url().'follow_from_'.urlencode($actor).'_to_'.urlencode($object), | ||||||
|  |             'type'   => 'Follow', | ||||||
|  |             'actor'  => $actor, | ||||||
|  |             'object' => $object | ||||||
|  |        ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Follow Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Profile $actor_profile Remote Actor | ||||||
|  |      * @param string $object Local Actor | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public static function follow($actor_profile, $object) | ||||||
|  |     { | ||||||
|  |         // Get Actor's Aprofile | ||||||
|  |         $actor_aprofile = Activitypub_profile::from_profile($actor_profile); | ||||||
|  |  | ||||||
|  |         // Get Object profile | ||||||
|  |         $object_profile = new Activitypub_explorer; | ||||||
|  |         $object_profile = $object_profile->lookup($object)[0]; | ||||||
|  |  | ||||||
|  |         if (!Subscription::exists($actor_profile, $object_profile)) { | ||||||
|  |             Subscription::start($actor_profile, $object_profile); | ||||||
|  |             common_debug('ActivityPubPlugin: Accepted Follow request from '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object); | ||||||
|  |         } else { | ||||||
|  |             common_debug('ActivityPubPlugin: Received a repeated Follow request from '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Notify remote instance that we have accepted their request | ||||||
|  |         common_debug('ActivityPubPlugin: Notifying remote instance that we have accepted their Follow request request from '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object); | ||||||
|  |         $postman = new Activitypub_postman($actor_profile, [$actor_aprofile]); | ||||||
|  |         $postman->accept_follow(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								classes/Activitypub_like.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										61
									
								
								classes/Activitypub_like.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub error representation | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_like extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Like | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $actor  Actor URI | ||||||
|  |      * @param string $object Notice URI | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function like_to_array($actor, $object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             'id'     => common_root_url().'like_from_'.urlencode($actor).'_to_'.urlencode($object), | ||||||
|  |             "type"   => "Like", | ||||||
|  |             "actor"  => $actor, | ||||||
|  |             "object" => $object | ||||||
|  |         ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								classes/Activitypub_mention_tag.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										60
									
								
								classes/Activitypub_mention_tag.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub Mention Tag representation | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_mention_tag extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Mention Tag | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $href Actor Uri | ||||||
|  |      * @param array $name Mention name | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function mention_tag_to_array_from_values($href, $name) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             "type" => "Mention", | ||||||
|  |             "href" => $href, | ||||||
|  |             "name" => $name | ||||||
|  |          ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										298
									
								
								classes/Activitypub_notice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										298
									
								
								classes/Activitypub_notice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -19,14 +19,13 @@ | |||||||
|  * |  * | ||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -34,56 +33,259 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  * |  * | ||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      http://www.gnu.org/software/social/ |  * @link      http://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| class Activitypub_notice extends Managed_DataObject | class Activitypub_notice extends Managed_DataObject | ||||||
| { | { | ||||||
|         /** |     /** | ||||||
|          * Generates a pretty notice from a Notice object |      * Generates a pretty notice from a Notice object | ||||||
|          * |      * | ||||||
|          * @param Notice $notice |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          * @return pretty array to be used in a response |      * @param Notice $notice | ||||||
|          */ |      * @return pretty array to be used in a response | ||||||
|         public static function notice_to_array ($notice) |      */ | ||||||
|         { |     public static function notice_to_array($notice) | ||||||
|                 $attachments = array (); |     { | ||||||
|                 foreach($notice->attachments () as $attachment) { |         $profile = $notice->getProfile(); | ||||||
|                         $attachments[] = Activitypub_attachment::attachment_to_array ($attachment); |         $attachments = []; | ||||||
|                 } |         foreach ($notice->attachments() as $attachment) { | ||||||
|  |             $attachments[] = Activitypub_attachment::attachment_to_array($attachment); | ||||||
|                 $tags = array (); |  | ||||||
|                 foreach($notice->getTags()as $tag) { |  | ||||||
|                         if ($tag != "") {       // Hacky workaround to avoid stupid outputs |  | ||||||
|                                 $tags[] = Activitypub_tag::tag_to_array ($tag); |  | ||||||
|                         } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $to = array (); |  | ||||||
|                 foreach ($notice->getAttentionProfileIDs () as $to_id) { |  | ||||||
|                         $to[] = Profile::getById ($to_id)->getUri (); |  | ||||||
|                 } |  | ||||||
|                 if (!is_null($to)) { |  | ||||||
|                         $to = array ("https://www.w3.org/ns/activitystreams#Public"); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $item = [ |  | ||||||
|                         'id'           => $notice->getUrl (), |  | ||||||
|                         'type'         => 'Notice', |  | ||||||
|                         'actor'        => $notice->getProfile ()->getUrl (), |  | ||||||
|                         'published'    => $notice->getCreated (), |  | ||||||
|                         'to'           => $to, |  | ||||||
|                         'content'      => $notice->getContent (), |  | ||||||
|                         'url'          => $notice->getUrl (), |  | ||||||
|                         'reply_to'     => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUrl (), |  | ||||||
|                         'is_local'     => $notice->isLocal (), |  | ||||||
|                         'conversation' => intval ($notice->conversation), |  | ||||||
|                         'attachment'   => $attachments, |  | ||||||
|                         'tag'          => $tags |  | ||||||
|                 ]; |  | ||||||
|  |  | ||||||
|                 return $item; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         $tags = []; | ||||||
|  |         foreach ($notice->getTags() as $tag) { | ||||||
|  |             if ($tag != "") {       // Hacky workaround to avoid stupid outputs | ||||||
|  |                 $tags[] = Activitypub_tag::tag_to_array($tag); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $cc = [common_local_url('apActorFollowers', ['id' => $profile->getID()])]; | ||||||
|  |         foreach ($notice->getAttentionProfiles() as $to_profile) { | ||||||
|  |             $cc[]  = $href = $to_profile->getUri(); | ||||||
|  |             $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // In a world without walls and fences, we should make everything Public! | ||||||
|  |         $to[]= 'https://www.w3.org/ns/activitystreams#Public'; | ||||||
|  |  | ||||||
|  |         $item = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             'id'           => common_local_url('apNotice', ['id' => $notice->getID()]), | ||||||
|  |             'type'         => 'Note', | ||||||
|  |             'published'    => str_replace(' ', 'T', $notice->getCreated()).'Z', | ||||||
|  |             'url'          => $notice->getUrl(), | ||||||
|  |             'attributedTo' => ActivityPubPlugin::actor_uri($profile), | ||||||
|  |             'to'           => ['https://www.w3.org/ns/activitystreams#Public'], | ||||||
|  |             'cc'           => $cc, | ||||||
|  |             'atomUri'      => $notice->getUrl(), | ||||||
|  |             'conversation' => $notice->getConversationUrl(), | ||||||
|  |             'content'      => $notice->getRendered(), | ||||||
|  |             'isLocal'      => $notice->isLocal(), | ||||||
|  |             'attachment'   => $attachments, | ||||||
|  |             'tag'          => $tags | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         // Is this a reply? | ||||||
|  |         if (!empty($notice->reply_to)) { | ||||||
|  |             $item['inReplyTo'] = common_local_url('apNotice', ['id' => $notice->getID()]); | ||||||
|  |             $item['inReplyToAtomUri'] = Notice::getById($notice->reply_to)->getUrl(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Do we have a location for this notice? | ||||||
|  |         try { | ||||||
|  |             $location = Notice_location::locFromStored($notice); | ||||||
|  |             $item['latitude']  = $location->lat; | ||||||
|  |             $item['longitude'] = $location->lon; | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             // Apparently no. | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $item; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Create a Notice via ActivityPub Note Object. | ||||||
|  |      * Returns created Notice. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Array $object | ||||||
|  |      * @param Profile|null $actor_profile | ||||||
|  |      * @return Notice | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public static function create_notice($object, $actor_profile = null) | ||||||
|  |     { | ||||||
|  |         $id      = $object['id'];         // int32 | ||||||
|  |         $url     = $object['url'];        // string | ||||||
|  |         $content = $object['content'];    // string | ||||||
|  |  | ||||||
|  |         // possible keys: ['inReplyTo', 'latitude', 'longitude', 'attachment'] | ||||||
|  |         $settings = []; | ||||||
|  |         if (isset($object['inReplyTo'])) { | ||||||
|  |             $settings['inReplyTo'] = $object['inReplyTo']; | ||||||
|  |         } | ||||||
|  |         if (isset($object['latitude'])) { | ||||||
|  |             $settings['latitude']  = $object['latitude']; | ||||||
|  |         } | ||||||
|  |         if (isset($object['longitude'])) { | ||||||
|  |             $settings['longitude'] = $object['longitude']; | ||||||
|  |         } | ||||||
|  |         if (isset($object['attachment'])) { | ||||||
|  |             $settings['attachment'] = $object['attachment']; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Ensure Actor Profile | ||||||
|  |         if (is_null($actor_profile)) { | ||||||
|  |             $actor_profile = ActivityPub_explorer::get_profile_from_url($object['actor']); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $act = new Activity(); | ||||||
|  |         $act->verb = ActivityVerb::POST; | ||||||
|  |         $act->time = time(); | ||||||
|  |         $act->actor = $actor_profile->asActivityObject(); | ||||||
|  |         $act->context = new ActivityContext(); | ||||||
|  |         $options = ['source' => 'ActivityPub', 'uri' => $id, 'url' => $url]; | ||||||
|  |  | ||||||
|  |         // Do we have an attachment? | ||||||
|  |         if (isset($settings['attachment'][0])) { | ||||||
|  |             $attach = $settings['attachment'][0]; | ||||||
|  |             $attach_url = $settings['attachment'][0]['url']; | ||||||
|  |             // Is it an image? | ||||||
|  |             if (ActivityPubPlugin::$store_images_from_remote_notes_attachments && substr($attach["mediaType"], 0, 5) == "image") { | ||||||
|  |                 $temp_filename = tempnam(sys_get_temp_dir(), 'apCreateNoteAttach_'); | ||||||
|  |                 try { | ||||||
|  |                     $imgData = HTTPClient::quickGet($attach_url); | ||||||
|  |                     // Make sure it's at least an image file. ImageFile can do the rest. | ||||||
|  |                     if (false === getimagesizefromstring($imgData)) { | ||||||
|  |                         common_debug('ActivityPub Create Notice: Failed because the downloaded image: '.$attach_url. 'is not valid.'); | ||||||
|  |                         throw new UnsupportedMediaException('Downloaded image was not an image.'); | ||||||
|  |                     } | ||||||
|  |                     file_put_contents($temp_filename, $imgData); | ||||||
|  |                     common_debug('ActivityPub Create Notice: Stored dowloaded image in: '.$temp_filename); | ||||||
|  |  | ||||||
|  |                     $id = $actor_profile->getID(); | ||||||
|  |  | ||||||
|  |                     $imagefile = new ImageFile(null, $temp_filename); | ||||||
|  |                     $filename = hash(File::FILEHASH_ALG, $imgData).image_type_to_extension($imagefile->type); | ||||||
|  |  | ||||||
|  |                     unset($imgData);    // No need to carry this in memory. | ||||||
|  |                     rename($temp_filename, File::path($filename)); | ||||||
|  |                     common_debug('ActivityPub Create Notice: Moved image from: '.$temp_filename.' to '.$filename); | ||||||
|  |                     $mediaFile = new MediaFile($filename, $attach['mediaType']); | ||||||
|  |                     $act->enclosures[] = $mediaFile->getEnclosure(); | ||||||
|  |                 } catch (Exception $e) { | ||||||
|  |                     common_debug('ActivityPub Create Notice: Something went wrong while processing the image from: '.$attach_url.' details: '.$e->getMessage()); | ||||||
|  |                     unlink($temp_filename); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             $content .= ($content==='' ? '' : ' ') . '<br><a href="'.$attach_url.'">Remote Attachment Source</a>'; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Is this a reply? | ||||||
|  |         if (isset($settings['inReplyTo'])) { | ||||||
|  |             try { | ||||||
|  |                 $inReplyTo = ActivityPubPlugin::grab_notice_from_url($settings['inReplyTo']); | ||||||
|  |                 $act->context->replyToID  = $inReplyTo->getUri(); | ||||||
|  |                 $act->context->replyToUrl = $inReplyTo->getUrl(); | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 // It failed to grab, maybe we got this note from another source | ||||||
|  |                 // (e.g.: OStatus) that handles this differently or we really | ||||||
|  |                 // failed to get it... | ||||||
|  |                 // Welp, nothing that we can do about, let's | ||||||
|  |                 // just fake we don't have such notice. | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             $inReplyTo = null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Mentions | ||||||
|  |         $mentions = []; | ||||||
|  |         if (isset($object['tag']) && is_array($object['tag'])) { | ||||||
|  |             foreach ($object['tag'] as $tag) { | ||||||
|  |                 if ($tag['type'] == 'Mention') { | ||||||
|  |                     $mentions[] = $tag['href']; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         $mentions_profiles = []; | ||||||
|  |         $discovery = new Activitypub_explorer; | ||||||
|  |         foreach ($mentions as $mention) { | ||||||
|  |             try { | ||||||
|  |                 $mentions_profiles[] = $discovery->lookup($mention)[0]; | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 // Invalid actor found, just let it go. // TODO: Fallback to OStatus | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         unset($discovery); | ||||||
|  |  | ||||||
|  |         foreach ($mentions_profiles as $mp) { | ||||||
|  |             $act->context->attention[ActivityPubPlugin::actor_uri($mp)] = 'http://activitystrea.ms/schema/1.0/person'; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Add location if that is set | ||||||
|  |         if (isset($settings['latitude'], $settings['longitude'])) { | ||||||
|  |             $act->context->location = Location::fromLatLon($settings['latitude'], $settings['longitude']); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Reject notice if it is too long (without the HTML) | ||||||
|  |         if (Notice::contentTooLong($content)) { | ||||||
|  |             //throw new Exception('That\'s too long. Maximum notice size is %d character.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $actobj = new ActivityObject(); | ||||||
|  |         $actobj->type = ActivityObject::NOTE; | ||||||
|  |         $actobj->content = strip_tags($content, '<p><b><i><u><a><ul><ol><li>'); | ||||||
|  |  | ||||||
|  |         // Finally add the activity object to our activity | ||||||
|  |         $act->objects[] = $actobj; | ||||||
|  |  | ||||||
|  |         $note = Notice::saveActivity($act, $actor_profile, $options); | ||||||
|  |         if (ActivityPubPlugin::$store_images_from_remote_notes_attachments && isset($mediaFile)) { | ||||||
|  |             $mediaFile->attachToNotice($note); | ||||||
|  |         } | ||||||
|  |         return $note; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Validates a note. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param  Array $object | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public static function validate_note($object) | ||||||
|  |     { | ||||||
|  |         if (!isset($object['attributedTo'])) { | ||||||
|  |             common_debug('ActivityPub Notice Validator: Rejected because attributedTo was not specified.'); | ||||||
|  |             throw new Exception('No attributedTo specified.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['id'])) { | ||||||
|  |             common_debug('ActivityPub Notice Validator: Rejected because Object ID was not specified.'); | ||||||
|  |             throw new Exception('Object ID not specified.'); | ||||||
|  |         } elseif (!filter_var($object['id'], FILTER_VALIDATE_URL)) { | ||||||
|  |             common_debug('ActivityPub Notice Validator: Rejected because Object ID is invalid.'); | ||||||
|  |             throw new Exception('Invalid Object ID.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['type']) || $object['type'] !== 'Note') { | ||||||
|  |             common_debug('ActivityPub Notice Validator: Rejected because of Type.'); | ||||||
|  |             throw new Exception('Invalid Object type.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['content'])) { | ||||||
|  |             common_debug('ActivityPub Notice Validator: Rejected because Content was not specified.'); | ||||||
|  |             throw new Exception('Object content was not specified.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['url'])) { | ||||||
|  |             throw new Exception('Object URL was not specified.'); | ||||||
|  |         } elseif (!filter_var($object['url'], FILTER_VALIDATE_URL)) { | ||||||
|  |             common_debug('ActivityPub Notice Validator: Rejected because Object URL is invalid.'); | ||||||
|  |             throw new Exception('Invalid Object URL.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['cc'])) { | ||||||
|  |             common_debug('ActivityPub Notice Validator: Rejected because Object CC was not specified.'); | ||||||
|  |             throw new Exception('Object CC was not specified.'); | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								classes/Activitypub_pending_follow_requests.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										117
									
								
								classes/Activitypub_pending_follow_requests.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub's Pending follow requests | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_pending_follow_requests extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     public $__table = 'Activitypub_pending_follow_requests'; | ||||||
|  |     public $local_profile_id; | ||||||
|  |     public $remote_profile_id; | ||||||
|  |     private $_reldb = null; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Return table definition for Schema setup and DB_DataObject usage. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return array array of column definitions | ||||||
|  |      */ | ||||||
|  |     public static function schemaDef() | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'fields' => [ | ||||||
|  |                 'local_profile_id'  => ['type' => 'integer', 'not null' => true], | ||||||
|  |                 'remote_profile_id' => ['type' => 'integer', 'not null' => true], | ||||||
|  |                 'relation_id'       => ['type' => 'serial',  'not null' => true], | ||||||
|  |             ], | ||||||
|  |             'primary key' => ['relation_id'], | ||||||
|  |             'unique keys' => [ | ||||||
|  |                 'Activitypub_pending_follow_requests_relation_id_key' => ['relation_id'], | ||||||
|  |             ], | ||||||
|  |             'foreign keys' => [ | ||||||
|  |                 'Activitypub_pending_follow_requests_local_profile_id_fkey'  => ['profile', ['local_profile_id' => 'id']], | ||||||
|  |                 'Activitypub_pending_follow_requests_remote_profile_id_fkey' => ['profile', ['remote_profile_id' => 'id']], | ||||||
|  |             ], | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function __construct($actor, $remote_actor) | ||||||
|  |     { | ||||||
|  |         $this->local_profile_id  = $actor; | ||||||
|  |         $this->remote_profile_id = $remote_actor; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Add Follow request to table. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param int32 $actor actor id | ||||||
|  |      * @param int32 $remote_actor remote actor id | ||||||
|  |      * @return boolean true if added, false otherwise | ||||||
|  |      */ | ||||||
|  |     public function add() | ||||||
|  |     { | ||||||
|  |         return !$this->exists() && $this->insert(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Check if a Follow request is pending. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return boolean true if is pending, false otherwise | ||||||
|  |      */ | ||||||
|  |     public function exists() | ||||||
|  |     { | ||||||
|  |         $this->_reldb = clone ($this); | ||||||
|  |         if ($this->_reldb->find() > 0) { | ||||||
|  |             $this->_reldb->fetch(); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Remove a request from the pending table. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return boolean true if removed, false otherwise | ||||||
|  |      */ | ||||||
|  |     public function remove() | ||||||
|  |     { | ||||||
|  |         return $this->exists() && $this->_reldb->delete(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										559
									
								
								classes/Activitypub_profile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										559
									
								
								classes/Activitypub_profile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,13 +20,12 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -35,170 +34,436 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      http://www.gnu.org/software/social/ |  * @link      http://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| class Activitypub_profile extends Profile | class Activitypub_profile extends Managed_DataObject | ||||||
| { | { | ||||||
|         public $__table = 'Activitypub_profile'; |     public $__table = 'Activitypub_profile'; | ||||||
|  |     public $uri;                             // text()   not_null | ||||||
|  |     public $profile_id;                      // int(4)  primary_key not_null | ||||||
|  |     public $inboxuri;                        // text()   not_null | ||||||
|  |     public $sharedInboxuri;                  // text() | ||||||
|  |     public $nickname;                        // varchar(64)  multiple_key not_null | ||||||
|  |     public $fullname;                        // text() | ||||||
|  |     public $profileurl;                      // text() | ||||||
|  |     public $homepage;                        // text() | ||||||
|  |     public $bio;                             // text()  multiple_key | ||||||
|  |     public $location;                        // text() | ||||||
|  |     public $created;                         // datetime()   not_null | ||||||
|  |     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP | ||||||
|  |  | ||||||
|         protected $_profile = null; |     /** | ||||||
|  |      * Return table definition for Schema setup and DB_DataObject usage. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return array array of column definitions | ||||||
|  |      */ | ||||||
|  |     public static function schemaDef() | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'fields' => [ | ||||||
|  |                 'uri' => ['type' => 'text', 'not null' => true], | ||||||
|  |                 'profile_id' => ['type' => 'integer'], | ||||||
|  |                 'inboxuri' => ['type' => 'text', 'not null' => true], | ||||||
|  |                 'sharedInboxuri' => ['type' => 'text'], | ||||||
|  |                 'created' => ['type' => 'datetime', 'not null' => true], | ||||||
|  |                 'modified' => ['type' => 'datetime', 'not null' => true], | ||||||
|  |             ], | ||||||
|  |             'primary key' => ['profile_id'], | ||||||
|  |             'unique keys' => [ | ||||||
|  |                 'Activitypub_profile_profile_id_key' => ['profile_id'], | ||||||
|  |             ], | ||||||
|  |             'foreign keys' => [ | ||||||
|  |                 'Activitypub_profile_profile_id_fkey' => ['profile', ['profile_id' => 'id']], | ||||||
|  |             ], | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|         /** |     /** | ||||||
|          * Return table definition for Schema setup and DB_DataObject usage. |      * Generates a pretty profile from a Profile object | ||||||
|          * |      * | ||||||
|          * @return array array of column definitions |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          */ |      * @param Profile $profile | ||||||
|         static function schemaDef () |      * @return pretty array to be used in a response | ||||||
|         { |      */ | ||||||
|             return array ( |     public static function profile_to_array($profile) | ||||||
|                 'fields' => array ( |     { | ||||||
|                     'uri' => array ('type' => 'varchar', 'length' => 191, 'not null' => true), |         $uri = ActivityPubPlugin::actor_uri($profile); | ||||||
|                     'profile_id' => array ('type' => 'integer'), |         $id = $profile->getID(); | ||||||
|                     'inboxuri' => array ('type' => 'varchar', 'length' => 191), |         $rsa = new Activitypub_rsa(); | ||||||
|                     'sharedInboxuri' => array ('type' => 'varchar', 'length' => 191), |         $public_key = $rsa->ensure_public_key($profile); | ||||||
|                     'created' => array ('type' => 'datetime', 'not null' => true), |         unset($rsa); | ||||||
|                     'modified' => array ('type' => 'datetime', 'not null' => true), |         $res = [ | ||||||
|                 ), |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|                 'primary key' => array ('uri'), |             'id'                => $uri, | ||||||
|                 'unique keys' => array ( |             'type'              => 'Person', | ||||||
|                     'Activitypub_profile_profile_id_key' => array ('profile_id'), |             'following'         => common_local_url('apActorFollowing', ['id' => $id]), | ||||||
|                     'Activitypub_profile_inboxuri_key' => array ('inboxuri'), |             'followers'         => common_local_url('apActorFollowers', ['id' => $id]), | ||||||
|                 ), |             'liked'             => common_local_url('apActorLiked', ['id' => $id]), | ||||||
|                 'foreign keys' => array ( |             'inbox'             => common_local_url('apInbox', ['id' => $id]), | ||||||
|                     'Activitypub_profile_profile_id_fkey' => array ('profile', array ('profile_id' => 'id')), |             'outbox'            => common_local_url('apActorOutbox', ['id' => $id]), | ||||||
|                 ), |             'preferredUsername' => $profile->getNickname(), | ||||||
|             ); |             'name'              => $profile->getBestName(), | ||||||
|  |             'summary'           => ($desc = $profile->getDescription()) == null ? "" : $desc, | ||||||
|  |             'url'               => $profile->getUrl(), | ||||||
|  |             'manuallyApprovesFollowers' => false, | ||||||
|  |             'publicKey' => [ | ||||||
|  |                 'id'    => $uri."#public-key", | ||||||
|  |                 'owner' => $uri, | ||||||
|  |                 'publicKeyPem' => $public_key | ||||||
|  |             ], | ||||||
|  |             'tag' => [], | ||||||
|  |             'attachment' => [], | ||||||
|  |             'icon' => [ | ||||||
|  |                 'type'      => 'Image', | ||||||
|  |                 'mediaType' => 'image/png', | ||||||
|  |                 'height'    => AVATAR_PROFILE_SIZE, | ||||||
|  |                 'width'     => AVATAR_PROFILE_SIZE, | ||||||
|  |                 'url'       => $profile->avatarUrl(AVATAR_PROFILE_SIZE) | ||||||
|  |             ] | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         if ($profile->isLocal()) { | ||||||
|  |             $res['endpoints']['sharedInbox'] = common_local_url('apInbox'); | ||||||
|  |         } else { | ||||||
|  |             $aprofile = new Activitypub_profile(); | ||||||
|  |             $aprofile = $aprofile->from_profile($profile); | ||||||
|  |             $res['endpoints']['sharedInbox'] = $aprofile->sharedInboxuri; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         return $res; | ||||||
|          * Generates a pretty profile from a Profile object |     } | ||||||
|          * |  | ||||||
|          * @param Profile $profile |     /** | ||||||
|          * @return pretty array to be used in a response |      * Insert the current object variables into the database | ||||||
|          */ |      * | ||||||
|         public static function profile_to_array ($profile) |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|         { |      * @access public | ||||||
|                 $url = $profile->getURL (); |      * @throws ServerException | ||||||
|                 $res = [ |      */ | ||||||
|                         '@context'        => [ |     public function do_insert() | ||||||
|                                 "https://www.w3.org/ns/activitystreams", |     { | ||||||
|                                 [ |         $profile = new Profile(); | ||||||
|                                         "@language"   => "en" |  | ||||||
|                                 ] |         $profile->created = $this->created = $this->modified = common_sql_now(); | ||||||
|                         ], |  | ||||||
|                         'id'              => $profile->getID (), |         $fields = [ | ||||||
|                         'type'            => 'Person', |                     'uri'      => 'profileurl', | ||||||
|                         'nickname'        => $profile->getNickname (), |                     'nickname' => 'nickname', | ||||||
|                         'is_local'        => $profile->isLocal (), |                     'fullname' => 'fullname', | ||||||
|                         'inbox'           => "{$url}/inbox.json", |                     'bio'      => 'bio' | ||||||
|                         'sharedInbox'     => common_root_url ()."inbox.json", |                     ]; | ||||||
|                         'outbox'          => "{$url}/outbox.json", |  | ||||||
|                         'display_name'    => $profile->getFullname (), |         foreach ($fields as $af => $pf) { | ||||||
|                         'followers'       => "{$url}/followers.json", |             $profile->$pf = $this->$af; | ||||||
|                         'followers_count' => $profile->subscriberCount (), |  | ||||||
|                         'following'       => "{$url}/following.json", |  | ||||||
|                         'following_count' => $profile->subscriptionCount (), |  | ||||||
|                         'liked'           => "{$url}/liked.json", |  | ||||||
|                         'liked_count'     => Fave::countByProfile ($profile), |  | ||||||
|                         'summary'         => ($desc = $profile->getDescription ()) == null ? "" : $desc, |  | ||||||
|                         'url'             => $profile->getURL (), |  | ||||||
|                         'avatar'          => [ |  | ||||||
|                                 'type'   => 'Image', |  | ||||||
|                                 'width'  => 96, |  | ||||||
|                                 'height' => 96, |  | ||||||
|                                 'url'    => $profile->avatarUrl (AVATAR_PROFILE_SIZE) |  | ||||||
|                         ] |  | ||||||
|                 ]; |  | ||||||
|                 return $res; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         $this->profile_id = $profile->insert(); | ||||||
|          * Insert the current objects variables into the database |         if ($this->profile_id === false) { | ||||||
|          * |             $profile->query('ROLLBACK'); | ||||||
|          * @access public |             throw new ServerException('Profile insertion failed.'); | ||||||
|          * @throws ServerException |  | ||||||
|          */ |  | ||||||
|         public function doInsert () |  | ||||||
|         { |  | ||||||
|                 $profile = new Profile (); |  | ||||||
|  |  | ||||||
|                 $profile->created = $this->created = $this->modified = common_sql_now (); |  | ||||||
|  |  | ||||||
|                 $fields = array ( |  | ||||||
|                             'uri'      => 'profileurl', |  | ||||||
|                             'nickname' => 'nickname', |  | ||||||
|                             'fullname' => 'fullname', |  | ||||||
|                             'bio'      => 'bio' |  | ||||||
|                             ); |  | ||||||
|  |  | ||||||
|                 foreach ($fields as $af => $pf) { |  | ||||||
|                         $profile->$pf = $this->$af; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $this->profile_id = $profile->insert (); |  | ||||||
|                 if ($this->profile_id === false) { |  | ||||||
|                         $profile->query ('ROLLBACK'); |  | ||||||
|                         throw new ServerException ('Profile insertion failed.'); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 $ok = $this->insert (); |  | ||||||
|  |  | ||||||
|                 if ($ok === false) { |  | ||||||
|                         $profile->query ('ROLLBACK'); |  | ||||||
|                         throw new ServerException ('Cannot save ActivityPub profile.'); |  | ||||||
|                 } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         $ok = $this->insert(); | ||||||
|          * Fetch the locally stored profile for this Activitypub_profile |  | ||||||
|          * @return Profile |         if ($ok === false) { | ||||||
|          * @throws NoProfileException if it was not found |             $profile->query('ROLLBACK'); | ||||||
|          */ |             $this->query('ROLLBACK'); | ||||||
|         public function localProfile () |             throw new ServerException('Cannot save ActivityPub profile.'); | ||||||
|         { |         } | ||||||
|                 $profile = Profile::getKV ('id', $this->profile_id); |     } | ||||||
|                 if (!$profile instanceof Profile) { |  | ||||||
|                         throw new NoProfileException ($this->profile_id); |     /** | ||||||
|                 } |      * Fetch the locally stored profile for this Activitypub_profile | ||||||
|                 return $profile; |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return Profile | ||||||
|  |      * @throws NoProfileException if it was not found | ||||||
|  |      */ | ||||||
|  |     public function local_profile() | ||||||
|  |     { | ||||||
|  |         $profile = Profile::getKV('id', $this->profile_id); | ||||||
|  |         if (!$profile instanceof Profile) { | ||||||
|  |             throw new NoProfileException($this->profile_id); | ||||||
|  |         } | ||||||
|  |         return $profile; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generates an Activitypub_profile from a Profile | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Profile $profile | ||||||
|  |      * @return Activitypub_profile | ||||||
|  |      * @throws Exception if no Activitypub_profile exists for given Profile | ||||||
|  |      */ | ||||||
|  |     public static function from_profile(Profile $profile) | ||||||
|  |     { | ||||||
|  |         $profile_id = $profile->getID(); | ||||||
|  |  | ||||||
|  |         $aprofile = self::getKV('profile_id', $profile_id); | ||||||
|  |         if (!$aprofile instanceof Activitypub_profile) { | ||||||
|  |             // No Activitypub_profile for this profile_id, | ||||||
|  |             if (!$profile->isLocal()) { | ||||||
|  |                 // create one! | ||||||
|  |                 $aprofile = self::create_from_local_profile($profile); | ||||||
|  |             } else { | ||||||
|  |                 throw new Exception('No Activitypub_profile for Profile ID: '.$profile_id. ', this is a local user.'); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         $fields = [ | ||||||
|          * Generates an Activitypub_profile from a Profile |                     'uri'      => 'profileurl', | ||||||
|          * |                     'nickname' => 'nickname', | ||||||
|          * @param Profile $profile |                     'fullname' => 'fullname', | ||||||
|          * @return Activitypub_profile |                     'bio'      => 'bio' | ||||||
|          * @throws Exception if no Activitypub_profile exists for given Profile |                     ]; | ||||||
|          */ |  | ||||||
|         static function fromProfile (Profile $profile) |  | ||||||
|         { |  | ||||||
|                 $profile_id = $profile->getID (); |  | ||||||
|  |  | ||||||
|                 $aprofile = Activitypub_profile::getKV ('profile_id', $profile_id); |         foreach ($fields as $af => $pf) { | ||||||
|                 if (!$aprofile instanceof Activitypub_profile) { |             $aprofile->$af = $profile->$pf; | ||||||
|                         throw new Exception('No Activitypub_profile for Profile ID: '.$profile_id); |         } | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 foreach ($profile as $key => $value) { |         return $aprofile; | ||||||
|                         $aprofile->$key = $value; |     } | ||||||
|                 } |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Given an existent local profile creates an ActivityPub profile. | ||||||
|  |      * One must be careful not to give a user profile to this function | ||||||
|  |      * as only remote users have ActivityPub_profiles on local instance | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Profile $profile | ||||||
|  |      * @return Activitypub_profile | ||||||
|  |      */ | ||||||
|  |     private static function create_from_local_profile(Profile $profile) | ||||||
|  |     { | ||||||
|  |         $url = $profile->getUri(); | ||||||
|  |         $inboxes = Activitypub_explorer::get_actor_inboxes_uri($url); | ||||||
|  |  | ||||||
|  |         if ($inboxes == null) { | ||||||
|  |             throw new Exception('This is not an ActivityPub user thus AProfile is politely refusing to proceed.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $aprofile->created = $aprofile->modified = common_sql_now(); | ||||||
|  |  | ||||||
|  |         $aprofile                 = new Activitypub_profile; | ||||||
|  |         $aprofile->profile_id     = $profile->getID(); | ||||||
|  |         $aprofile->uri            = $url; | ||||||
|  |         $aprofile->nickname       = $profile->getNickname(); | ||||||
|  |         $aprofile->fullname       = $profile->getFullname(); | ||||||
|  |         $aprofile->bio            = substr($profile->getDescription(), 0, 1000); | ||||||
|  |         $aprofile->inboxuri       = $inboxes["inbox"]; | ||||||
|  |         $aprofile->sharedInboxuri = $inboxes["sharedInbox"]; | ||||||
|  |  | ||||||
|  |         $aprofile->insert(); | ||||||
|  |  | ||||||
|  |         return $aprofile; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns sharedInbox if possible, inbox otherwise | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return string Inbox URL | ||||||
|  |      */ | ||||||
|  |     public function get_inbox() | ||||||
|  |     { | ||||||
|  |         if (is_null($this->sharedInboxuri)) { | ||||||
|  |             return $this->inboxuri; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $this->sharedInboxuri; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Getter for uri property | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return string URI | ||||||
|  |      */ | ||||||
|  |     public function getUri() | ||||||
|  |     { | ||||||
|  |         return $this->uri; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Getter for url property | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return string URL | ||||||
|  |      */ | ||||||
|  |     public function getUrl() | ||||||
|  |     { | ||||||
|  |         return $this->getUri(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Getter for id property | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return int32 | ||||||
|  |      */ | ||||||
|  |     public function getID() | ||||||
|  |     { | ||||||
|  |         return $this->profile_id; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Ensures a valid Activitypub_profile when provided with a valid URI. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $url | ||||||
|  |      * @return Activitypub_profile | ||||||
|  |      * @throws Exception if it isn't possible to return an Activitypub_profile | ||||||
|  |      */ | ||||||
|  |     public static function fromUri($url) | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             return self::from_profile(Activitypub_explorer::get_profile_from_url($url)); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             throw new Exception('No valid ActivityPub profile found for given URI.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Look up, and if necessary create, an Activitypub_profile for the remote | ||||||
|  |      * entity with the given webfinger address. | ||||||
|  |      * This should never return null -- you will either get an object or | ||||||
|  |      * an exception will be thrown. | ||||||
|  |      * | ||||||
|  |      * @author GNU Social | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $addr webfinger address | ||||||
|  |      * @return Activitypub_profile | ||||||
|  |      * @throws Exception on error conditions | ||||||
|  |      */ | ||||||
|  |     public static function ensure_web_finger($addr) | ||||||
|  |     { | ||||||
|  |         // Normalize $addr, i.e. add 'acct:' if missing | ||||||
|  |         $addr = Discovery::normalize($addr); | ||||||
|  |  | ||||||
|  |         // Try the cache | ||||||
|  |         $uri = self::cacheGet(sprintf('activitypub_profile:webfinger:%s', $addr)); | ||||||
|  |  | ||||||
|  |         if ($uri !== false) { | ||||||
|  |             if (is_null($uri)) { | ||||||
|  |                 // Negative cache entry | ||||||
|  |                 // TRANS: Exception. | ||||||
|  |                 throw new Exception(_m('Not a valid webfinger address (via cache).')); | ||||||
|  |             } | ||||||
|  |             try { | ||||||
|  |                 return self::fromUri($uri); | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 common_log(LOG_ERR, sprintf(__METHOD__ . ': Webfinger address cache inconsistent with database, did not find Activitypub_profile uri==%s', $uri)); | ||||||
|  |                 self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), false); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Now, try some discovery | ||||||
|  |  | ||||||
|  |         $disco = new Discovery(); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             $xrd = $disco->lookup($addr); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             // Save negative cache entry so we don't waste time looking it up again. | ||||||
|  |             // @todo FIXME: Distinguish temporary failures? | ||||||
|  |             self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), null); | ||||||
|  |             // TRANS: Exception. | ||||||
|  |             throw new Exception(_m('Not a valid webfinger address.')); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $hints = array_merge( | ||||||
|  |                     array('webfinger' => $addr), | ||||||
|  |                 DiscoveryHints::fromXRD($xrd) | ||||||
|  |                 ); | ||||||
|  |  | ||||||
|  |         // If there's an Hcard, let's grab its info | ||||||
|  |         if (array_key_exists('hcard', $hints)) { | ||||||
|  |             if (!array_key_exists('profileurl', $hints) || | ||||||
|  |                         $hints['hcard'] != $hints['profileurl']) { | ||||||
|  |                 $hcardHints = DiscoveryHints::fromHcardUrl($hints['hcard']); | ||||||
|  |                 $hints = array_merge($hcardHints, $hints); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // If we got a profile page, try that! | ||||||
|  |         $profileUrl = null; | ||||||
|  |         if (array_key_exists('profileurl', $hints)) { | ||||||
|  |             $profileUrl = $hints['profileurl']; | ||||||
|  |             try { | ||||||
|  |                 common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl"); | ||||||
|  |                 $aprofile = self::fromUri($hints['profileurl']); | ||||||
|  |                 self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), $aprofile->getUri()); | ||||||
|                 return $aprofile; |                 return $aprofile; | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage()); | ||||||
|  |                 // keep looking | ||||||
|  |                                 // | ||||||
|  |                                 // @todo FIXME: This means an error discovering from profile page | ||||||
|  |                                 // may give us a corrupt entry using the webfinger URI, which | ||||||
|  |                                 // will obscure the correct page-keyed profile later on. | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         // XXX: try hcard | ||||||
|          * Returns sharedInbox if possible, inbox otherwise |         // XXX: try FOAF | ||||||
|          * |  | ||||||
|          * @return string Inbox URL |  | ||||||
|          */ |  | ||||||
|         public function getInbox () |  | ||||||
|         { |  | ||||||
|                 if (is_null ($this->sharedInboxuri)) { |  | ||||||
|                         return $this->inboxuri; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return $this->sharedInboxuri; |         // TRANS: Exception. %s is a webfinger address. | ||||||
|  |         throw new Exception(sprintf(_m('Could not find a valid profile for "%s".'), $addr)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Update remote user profile in local instance | ||||||
|  |      * Depends on do_update | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $res remote response | ||||||
|  |      * @return Profile remote Profile object | ||||||
|  |      */ | ||||||
|  |     public static function update_profile($aprofile, $res) | ||||||
|  |     { | ||||||
|  |         // ActivityPub Profile | ||||||
|  |         $aprofile->uri            = $res['id']; | ||||||
|  |         $aprofile->nickname       = $res['preferredUsername']; | ||||||
|  |         $aprofile->fullname       = isset($res['name']) ? $res['name'] : null; | ||||||
|  |         $aprofile->bio            = isset($res['summary']) ? substr(strip_tags($res['summary']), 0, 1000) : null; | ||||||
|  |         $aprofile->inboxuri       = $res['inbox']; | ||||||
|  |         $aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']; | ||||||
|  |  | ||||||
|  |         $profile = $aprofile->local_profile(); | ||||||
|  |  | ||||||
|  |         $profile->modified = $aprofile->modified = common_sql_now(); | ||||||
|  |  | ||||||
|  |         $fields = [ | ||||||
|  |                     'uri'      => 'profileurl', | ||||||
|  |                     'nickname' => 'nickname', | ||||||
|  |                     'fullname' => 'fullname', | ||||||
|  |                     'bio'      => 'bio' | ||||||
|  |                     ]; | ||||||
|  |  | ||||||
|  |         foreach ($fields as $af => $pf) { | ||||||
|  |             $profile->$pf = $aprofile->$af; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // Profile | ||||||
|  |         $profile->update(); | ||||||
|  |         $aprofile->update(); | ||||||
|  |  | ||||||
|  |         // Public Key | ||||||
|  |         Activitypub_rsa::update_public_key($profile, $res['publicKey']['publicKeyPem']); | ||||||
|  |  | ||||||
|  |         // Avatar | ||||||
|  |         if (isset($res['icon']['url'])) { | ||||||
|  |             try { | ||||||
|  |                 Activitypub_explorer::update_avatar($profile, $res['icon']['url']); | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 // Let the exception go, it isn't a serious issue | ||||||
|  |                 common_debug('An error ocurred while grabbing remote avatar'.$e->getMessage()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $profile; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								actions/inbox/Like.php → classes/Activitypub_reject.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										41
									
								
								actions/inbox/Like.php → classes/Activitypub_reject.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,22 +20,39 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| try { | /** | ||||||
|         Fave::addNew ($actor_profile, Notice::getByUri ($data->object)); |  * ActivityPub error representation | ||||||
|         $res = array ("@context" => "https://www.w3.org/ns/activitystreams", |  * | ||||||
|                   "type"   => "Like", |  * @category  Plugin | ||||||
|                   "actor"  => $data->actor, |  * @package   GNUsocial | ||||||
|                   "object" => $data->object); |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|         ActivityPubReturn::answer ($res); |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
| } catch (Exception $e) { |  * @link      http://www.gnu.org/software/social/ | ||||||
|         ActivityPubReturn::error ($e->getMessage (), 403); |  */ | ||||||
|  | class Activitypub_reject extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Reject | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $object | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function reject_to_array($object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |                 '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |                 "type"   => "Reject", | ||||||
|  |                 "object" => $object | ||||||
|  |         ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
| } | } | ||||||
							
								
								
									
										178
									
								
								classes/Activitypub_rsa.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										178
									
								
								classes/Activitypub_rsa.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,178 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub Keys System | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_rsa extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     public $__table = 'Activitypub_rsa'; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Return table definition for Schema setup and DB_DataObject usage. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return array array of column definitions | ||||||
|  |      */ | ||||||
|  |     public static function schemaDef() | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |                 'fields' => [ | ||||||
|  |                     'profile_id'  => ['type' => 'integer'], | ||||||
|  |                     'private_key' => ['type' => 'text'], | ||||||
|  |                     'public_key'  => ['type' => 'text', 'not null' => true], | ||||||
|  |                     'created'     => ['type' => 'datetime', 'not null' => true], | ||||||
|  |                     'modified'    => ['type' => 'datetime', 'not null' => true], | ||||||
|  |                 ], | ||||||
|  |                 'primary key' => ['profile_id'], | ||||||
|  |                 'unique keys' => [ | ||||||
|  |                     'Activitypub_rsa_profile_id_key'  => ['profile_id'], | ||||||
|  |                 ], | ||||||
|  |                 'foreign keys' => [ | ||||||
|  |                     'Activitypub_profile_profile_id_fkey' => ['profile', ['profile_id' => 'id']], | ||||||
|  |                 ], | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function get_private_key($profile) | ||||||
|  |     { | ||||||
|  |         $this->profile_id = $profile->getID(); | ||||||
|  |         $apRSA = self::getKV('profile_id', $this->profile_id); | ||||||
|  |         if (!$apRSA instanceof Activitypub_rsa) { | ||||||
|  |             // No existing key pair for this profile | ||||||
|  |             if ($profile->isLocal()) { | ||||||
|  |                 self::generate_keys($this->private_key, $this->public_key); | ||||||
|  |                 $this->store_keys(); | ||||||
|  |             } else { | ||||||
|  |                 throw new Exception('This is a remote Profile, there is no Private Key for this Profile.'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $apRSA->private_key; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Guarantees a Public Key for a given profile. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Profile $profile | ||||||
|  |      * @return string The public key | ||||||
|  |      * @throws ServerException It should never occur, but if so, we break everything! | ||||||
|  |      */ | ||||||
|  |     public function ensure_public_key($profile, $fetch = true) | ||||||
|  |     { | ||||||
|  |         $this->profile_id = $profile->getID(); | ||||||
|  |         $apRSA = self::getKV('profile_id', $this->profile_id); | ||||||
|  |         if (!$apRSA instanceof Activitypub_rsa) { | ||||||
|  |             // No existing key pair for this profile | ||||||
|  |             if ($profile->isLocal()) { | ||||||
|  |                 self::generate_keys($this->private_key, $this->public_key); | ||||||
|  |                 $this->store_keys(); | ||||||
|  |             } else { | ||||||
|  |                 // This should never happen, but try to recover! | ||||||
|  |                 if ($fetch) { | ||||||
|  |                     $res = Activitypub_explorer::get_remote_user_activity(ActivityPubPlugin::actor_uri($profile)); | ||||||
|  |                     Activitypub_rsa::update_public_key($profile, $res['publicKey']['publicKeyPem']); | ||||||
|  |                     return ensure_public_key($profile, false); | ||||||
|  |                 } else { | ||||||
|  |                     throw new ServerException('Activitypub_rsa: Failed to find keys for given profile. That should have not happened!'); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $apRSA->public_key; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Insert the current object variables into the database. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @access public | ||||||
|  |      * @throws ServerException | ||||||
|  |      */ | ||||||
|  |     public function store_keys() | ||||||
|  |     { | ||||||
|  |         $this->created = $this->modified = common_sql_now(); | ||||||
|  |         $ok = $this->insert(); | ||||||
|  |         if ($ok === false) { | ||||||
|  |             throw new ServerException('Cannot save ActivityPub RSA.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generates a pair of RSA keys. | ||||||
|  |      * | ||||||
|  |      * @author PHP Manual Contributed Notes <dirt@awoms.com> | ||||||
|  |      * @param string $private_key in/out | ||||||
|  |      * @param string $public_key in/out | ||||||
|  |      */ | ||||||
|  |     public static function generate_keys(&$private_key, &$public_key) | ||||||
|  |     { | ||||||
|  |         $config = [ | ||||||
|  |             'digest_alg'       => 'sha512', | ||||||
|  |             'private_key_bits' => 2048, | ||||||
|  |             'private_key_type' => OPENSSL_KEYTYPE_RSA, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         // Create the private and public key | ||||||
|  |         $res = openssl_pkey_new($config); | ||||||
|  |  | ||||||
|  |         // Extract the private key from $res to $private_key | ||||||
|  |         openssl_pkey_export($res, $private_key); | ||||||
|  |  | ||||||
|  |         // Extract the public key from $res to $pubKey | ||||||
|  |         $pubKey = openssl_pkey_get_details($res); | ||||||
|  |         $public_key = $pubKey["key"]; | ||||||
|  |         unset($pubKey); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Update public key. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Profile $profile | ||||||
|  |      * @param string $public_key | ||||||
|  |      */ | ||||||
|  |     public static function update_public_key($profile, $public_key) | ||||||
|  |     { | ||||||
|  |         // Public Key | ||||||
|  |         $apRSA = new Activitypub_rsa(); | ||||||
|  |         $apRSA->profile_id = $profile->getID(); | ||||||
|  |         $apRSA->public_key = $public_key; | ||||||
|  |         $apRSA->modified = common_sql_now(); | ||||||
|  |         if(!$apRSA->update()) { | ||||||
|  |             $apRSA->insert(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								classes/Activitypub_tag.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										42
									
								
								classes/Activitypub_tag.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,13 +20,12 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -40,25 +39,20 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  */ |  */ | ||||||
| class Activitypub_tag extends Managed_DataObject | class Activitypub_tag extends Managed_DataObject | ||||||
| { | { | ||||||
|         /** |     /** | ||||||
|          * Generates a pretty tag from a Tag object |      * Generates a pretty tag from a Tag object | ||||||
|          * |      * | ||||||
|          * @param Tag $tag |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          * @return pretty array to be used in a response |      * @param Tag $tag | ||||||
|          */ |      * @return pretty array to be used in a response | ||||||
|         public static function tag_to_array ($tag) |      */ | ||||||
|         { |     public static function tag_to_array($tag) | ||||||
|                 $res = [ |     { | ||||||
|                 '@context'          => [ |         $res = [ | ||||||
|                 "https://www.w3.org/ns/activitystreams", |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|                 [ |             'name' => $tag, | ||||||
|                   "@language" => "en" |             'url'  => common_local_url('tag', ['tag' => $tag]) | ||||||
|                 ] |         ]; | ||||||
|                 ], |         return $res; | ||||||
|                 'name' => $tag, |     } | ||||||
|                 'url'  => common_local_url ('tag', array('tag' => $tag)) |  | ||||||
|                 ]; |  | ||||||
|  |  | ||||||
|                 return $res; |  | ||||||
|         } |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								classes/Activitypub_undo.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										89
									
								
								classes/Activitypub_undo.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub error representation | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_undo extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Generates an ActivityPub representation of a Undo | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $object | ||||||
|  |      * @return pretty array to be used in a response | ||||||
|  |      */ | ||||||
|  |     public static function undo_to_array($object) | ||||||
|  |     { | ||||||
|  |         $res = [ | ||||||
|  |             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||||
|  |             'id'     => $object['id'].'/undo', | ||||||
|  |             'type'   => 'Undo', | ||||||
|  |             'actor'  => $object['actor'], | ||||||
|  |             'object' => $object | ||||||
|  |         ]; | ||||||
|  |         return $res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Verifies if a given object is acceptable for a Undo Activity. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Array $object | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public static function validate_object($object) | ||||||
|  |     { | ||||||
|  |         if (!is_array($object)) { | ||||||
|  |             throw new Exception('Invalid Object Format for Undo Activity.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($object['type'])) { | ||||||
|  |             throw new Exception('Object type was not specified for Undo Activity.'); | ||||||
|  |         } | ||||||
|  |         switch ($object['type']) { | ||||||
|  |             case 'Follow': | ||||||
|  |             case 'Like': | ||||||
|  |                 // Validate data | ||||||
|  |                 if (!filter_var($object['object'], FILTER_VALIDATE_URL)) { | ||||||
|  |                     throw new Exception('Object is not a valid Object URI for Activity.'); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new Exception('This is not a supported Object Type for Undo Activity.'); | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								composer.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										24
									
								
								composer.json
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | { | ||||||
|  |     "name": "dansup/activity-pub", | ||||||
|  |     "description": "ActivityPub plugin for GNU/Social", | ||||||
|  |     "type": "gnusocial-plugin", | ||||||
|  |     "require": { | ||||||
|  |         "pixelfed/http-signatures-guzzlehttp": "^4.0" | ||||||
|  |     }, | ||||||
|  |     "license": "AGPL", | ||||||
|  |     "autoload": { | ||||||
|  |         "psr-4": { | ||||||
|  |             "Tests\\": "tests/" | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "authors": [ | ||||||
|  |         { | ||||||
|  |             "name": "Daniel Supernault", | ||||||
|  |             "email": "danielsupernault@gmail.com" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "name": "Diogo Cordeiro", | ||||||
|  |             "email": "diogo@fc.up.pt" | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										104
									
								
								daemons/update_activitypub_profiles.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										104
									
								
								daemons/update_activitypub_profiles.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | #!/usr/bin/env php | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | define('INSTALLDIR', realpath(__DIR__ . '/../../..')); | ||||||
|  |  | ||||||
|  | $shortoptions = 'u:af'; | ||||||
|  | $longoptions = ['uri=', 'all', 'force']; | ||||||
|  |  | ||||||
|  | $helptext = <<<END_OF_HELP | ||||||
|  | update_activitypub_profiles.php [options] | ||||||
|  | Refetch / update ActivityPub RSA keys, profile info and avatars. Useful if you | ||||||
|  | do something like accidentally delete your avatars directory when | ||||||
|  | you have no backup. | ||||||
|  |  | ||||||
|  |     -u --uri ActivityPub profile URI to update | ||||||
|  |     -a --all update all | ||||||
|  |  | ||||||
|  | END_OF_HELP; | ||||||
|  |  | ||||||
|  | require_once INSTALLDIR.'/scripts/commandline.inc'; | ||||||
|  |  | ||||||
|  | $quiet = have_option('q', 'quiet'); | ||||||
|  |  | ||||||
|  | if (!$quiet) { | ||||||
|  |     echo "ActivityPub Profiles updater will now start!\n"; | ||||||
|  |     echo "Summoning Diogo Cordeiro, Richard Stallman and Chuck Norris to help us with this task!\n"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | if (have_option('u', 'uri')) { | ||||||
|  |     $uri = get_option_value('u', 'uri'); | ||||||
|  |     $discovery = new Activitypub_explorer(); | ||||||
|  |     $discovery = $discovery->lookup($uri); | ||||||
|  |     if (empty($discovery)) { | ||||||
|  |         echo "Bad URI\n"; | ||||||
|  |         exit(1); | ||||||
|  |     } | ||||||
|  |     $user = $discovery->lookup($uri)[0]; | ||||||
|  |     try { | ||||||
|  |         $res = Activitypub_explorer::get_remote_user_activity($uri); | ||||||
|  |     } catch (Exception $e) { | ||||||
|  |         echo $e->getMessage()."\n"; | ||||||
|  |         exit(1); | ||||||
|  |     } | ||||||
|  |     if (!$quiet) { | ||||||
|  |         echo "Updated ".Activitypub_profile::update_profile($user, $res)->getBestName()."\n"; | ||||||
|  |     } | ||||||
|  | } else if (!have_option('a', 'all')) { | ||||||
|  |     show_help(); | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $user = new Activitypub_profile(); | ||||||
|  | $cnt = $user->find(); | ||||||
|  | if (!empty($cnt)) { | ||||||
|  |     if (!$quiet) { | ||||||
|  |         echo "Found {$cnt} ActivityPub profiles:\n"; | ||||||
|  |     } | ||||||
|  | } else { | ||||||
|  |     if (have_option('u', 'uri')) { | ||||||
|  |         if (!$quiet) { | ||||||
|  |             echo "Couldn't find an existing ActivityPub profile with that URI.\n"; | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         if (!$quiet) { | ||||||
|  |             echo "Couldn't find any existing ActivityPub profiles.\n"; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     exit(0); | ||||||
|  | } | ||||||
|  | while ($user->fetch()) { | ||||||
|  |     try { | ||||||
|  |         $res = Activitypub_explorer::get_remote_user_activity($user->uri); | ||||||
|  |         if (!$quiet) { | ||||||
|  |             echo "Updated ".Activitypub_profile::update_profile($user, $res)->getBestName()."\n"; | ||||||
|  |         } | ||||||
|  |     } catch (Exception $e) { | ||||||
|  |         // let it go | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								phpunit.xml
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										25
									
								
								phpunit.xml
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <phpunit backupGlobals="false" | ||||||
|  |          backupStaticAttributes="false" | ||||||
|  |          bootstrap="vendor/autoload.php" | ||||||
|  |          colors="true" | ||||||
|  |          convertErrorsToExceptions="true" | ||||||
|  |          convertNoticesToExceptions="true" | ||||||
|  |          convertWarningsToExceptions="true" | ||||||
|  |          processIsolation="false" | ||||||
|  |          stopOnFailure="false"> | ||||||
|  |     <testsuites> | ||||||
|  |         <testsuite name="Feature"> | ||||||
|  |             <directory suffix="Test.php">./tests/Feature</directory> | ||||||
|  |         </testsuite> | ||||||
|  |  | ||||||
|  |         <testsuite name="Unit"> | ||||||
|  |             <directory suffix="Test.php">./tests/Unit</directory> | ||||||
|  |         </testsuite> | ||||||
|  |     </testsuites> | ||||||
|  |     <filter> | ||||||
|  |         <whitelist processUncoveredFilesFromWhitelist="true"> | ||||||
|  |             <directory suffix=".php">./app</directory> | ||||||
|  |         </whitelist> | ||||||
|  |     </filter> | ||||||
|  | </phpunit> | ||||||
							
								
								
									
										28
									
								
								tests/CreatesApplication.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										28
									
								
								tests/CreatesApplication.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Tests; | ||||||
|  |  | ||||||
|  | trait CreatesApplication | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Creates the application. | ||||||
|  |      * | ||||||
|  |      * @return todo | ||||||
|  |      */ | ||||||
|  |     public static function createApplication() | ||||||
|  |     { | ||||||
|  |         if (!defined('INSTALLDIR')) { | ||||||
|  |             define('INSTALLDIR', __DIR__ . '/../../../'); | ||||||
|  |         } | ||||||
|  |         if (!defined('GNUSOCIAL')) { | ||||||
|  |             define('GNUSOCIAL', true); | ||||||
|  |         } | ||||||
|  |         if (!defined('STATUSNET')) { | ||||||
|  |             define('STATUSNET', true);  // compatibility | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         require INSTALLDIR . '/lib/common.php'; | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								tests/TestCase.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										15
									
								
								tests/TestCase.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Tests; | ||||||
|  |  | ||||||
|  | use PHPUnit\Framework\TestCase as BaseTestCase; | ||||||
|  |  | ||||||
|  | abstract class TestCase extends BaseTestCase | ||||||
|  | { | ||||||
|  |     use CreatesApplication; | ||||||
|  |  | ||||||
|  |     protected function setUp() | ||||||
|  |     { | ||||||
|  |         $this->createApplication(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								tests/Unit/AcceptHeaderTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								tests/Unit/AcceptHeaderTest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | require 'AcceptHeader.php'; | ||||||
|  |  | ||||||
|  | class ContainerTest extends \PHPUnit_Framework_TestCase | ||||||
|  | { | ||||||
|  |     public function testHeader1() | ||||||
|  |     { | ||||||
|  |         $acceptHeader = new AcceptHeader('audio/*; q=0.2, audio/basic'); | ||||||
|  |         $this->assertEquals('audio/basic', $this->_getMedia($acceptHeader[0])); | ||||||
|  |         $this->assertEquals('audio/*; q=0.2', $this->_getMedia($acceptHeader[1])); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function testHeader2() | ||||||
|  |     { | ||||||
|  |         $acceptHeader = new AcceptHeader('text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'); | ||||||
|  |         $this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0])); | ||||||
|  |         $this->assertEquals('text/html; q=0.7', $this->_getMedia($acceptHeader[1])); | ||||||
|  |         $this->assertEquals('*/*; q=0.5', $this->_getMedia($acceptHeader[2])); | ||||||
|  |         $this->assertEquals('text/html; level=2; q=0.4', $this->_getMedia($acceptHeader[3])); | ||||||
|  |         $this->assertEquals('text/*; q=0.3', $this->_getMedia($acceptHeader[4])); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function testHeader3() | ||||||
|  |     { | ||||||
|  |         $acceptHeader = new AcceptHeader('text/*, text/html, text/html;level=1, */*'); | ||||||
|  |         $this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0])); | ||||||
|  |         $this->assertEquals('text/html', $this->_getMedia($acceptHeader[1])); | ||||||
|  |         $this->assertEquals('text/*', $this->_getMedia($acceptHeader[2])); | ||||||
|  |         $this->assertEquals('*/*', $this->_getMedia($acceptHeader[3])); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private function _getMedia(array $mediaType) | ||||||
|  |     { | ||||||
|  |         $str = $mediaType['type'] . '/' . $mediaType['subtype']; | ||||||
|  |         if (!empty($mediaType['params'])) { | ||||||
|  |             foreach ($mediaType['params'] as $k => $v) { | ||||||
|  |                 $str .= '; ' . $k . '=' . $v; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $str; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										159
									
								
								tests/Unit/ActivitypubProfileTest.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										159
									
								
								tests/Unit/ActivitypubProfileTest.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,159 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Tests\Unit; | ||||||
|  |  | ||||||
|  | use Tests\TestCase; | ||||||
|  |  | ||||||
|  | class ProfileObjectTest extends TestCase | ||||||
|  | { | ||||||
|  |     public function testLibraryInstalled() | ||||||
|  |     { | ||||||
|  |         $this->assertTrue(class_exists('\Activitypub_profile')); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function testActivitypubProfile() | ||||||
|  |     { | ||||||
|  |         // Mimic proper ACCEPT header | ||||||
|  |         $_SERVER['HTTP_ACCEPT'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams'; | ||||||
|  |  | ||||||
|  |         /* Test do_insert() */ | ||||||
|  |         $aprofile = new \Activitypub_profile(); | ||||||
|  |         $aprofile->uri = 'https://testinstance.net/index.php/user/1'; | ||||||
|  |         $aprofile->nickname = 'test1'; | ||||||
|  |         $aprofile->fullname = 'Test User 1'; | ||||||
|  |         $aprofile->bio      = 'I am a nice test 1 guy'; | ||||||
|  |         $aprofile->inboxuri = "https://testinstance.net/index.php/user/1/inbox.json"; | ||||||
|  |         $aprofile->sharedInboxuri = "https://testinstance.net/inbox.json"; | ||||||
|  |         $aprofile->do_insert(); | ||||||
|  |  | ||||||
|  |         /* Test local_profile() */ | ||||||
|  |         $profile = $aprofile->local_profile(); | ||||||
|  |  | ||||||
|  |         /* Test from_profile() and create_from_local_profile() */ | ||||||
|  |         $this->assertTrue($this->compare_aprofiles($aprofile, \Activitypub_profile::from_profile($profile))); | ||||||
|  |  | ||||||
|  |         /* Create Keys for Test User 1 */ | ||||||
|  |         $apRSA = new \Activitypub_rsa(); | ||||||
|  |         $apRSA->profile_id = $profile->getID(); | ||||||
|  |         \Activitypub_rsa::generate_keys($apRSA->private_key, $apRSA->public_key); | ||||||
|  |         $apRSA->store_keys(); | ||||||
|  |  | ||||||
|  |         /* Test profile_to_array() */ | ||||||
|  |         // Fetch ActivityPub Actor Object representation | ||||||
|  |         $profile_array = \Activitypub_profile::profile_to_array($profile); | ||||||
|  |         // Check type | ||||||
|  |         $this->assertTrue(is_array($profile_array)); | ||||||
|  |         // Test with Explorer's Profile Tester | ||||||
|  |         $this->assertTrue(\Activitypub_explorer::validate_remote_response($profile_array)); | ||||||
|  |  | ||||||
|  |         /* Test get_inbox() */ | ||||||
|  |         $this->assertTrue($aprofile->sharedInboxuri == $aprofile->get_inbox()); | ||||||
|  |  | ||||||
|  |         /* Test getUri() */ | ||||||
|  |         $this->assertTrue($aprofile->uri == $aprofile->getUri()); | ||||||
|  |  | ||||||
|  |         /* Test getUrl() */ | ||||||
|  |         $this->assertTrue($profile->getUrl() == $aprofile->getUrl()); | ||||||
|  |  | ||||||
|  |         /* Test getID() */ | ||||||
|  |         $this->assertTrue($profile->getID() == $aprofile->getID()); | ||||||
|  |  | ||||||
|  |         /* Test fromUri() */ | ||||||
|  |         $this->assertTrue($this->compare_aprofiles($aprofile, \Activitypub_profile::fromUri($aprofile->uri))); | ||||||
|  |  | ||||||
|  |         /* Remove Remote User Test 1 */ | ||||||
|  |         $old_id = $profile->getID(); | ||||||
|  |         $apRSA->delete(); | ||||||
|  |         $aprofile->delete(); | ||||||
|  |         $profile->delete(); | ||||||
|  |         // Check if successfuly removed | ||||||
|  |         try { | ||||||
|  |             \Profile::getById($old_id); | ||||||
|  |             $this->assertTrue(false); | ||||||
|  |         } catch (\NoResultException $e) { | ||||||
|  |             $this->assertTrue(true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* Test ensure_web_finger() */ | ||||||
|  |         // TODO: Maybe elaborate on this function's tests | ||||||
|  |         try { | ||||||
|  |             \Activitypub_profile::ensure_web_finger('test1@testinstance.net'); | ||||||
|  |             $this->assertTrue(false); | ||||||
|  |         } catch (\Exception $e) { | ||||||
|  |             $this->assertTrue($e->getMessage() == 'Not a valid webfinger address.' || | ||||||
|  |                               $e->getMessage() == 'Not a valid webfinger address (via cache).'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Helpers | ||||||
|  |  | ||||||
|  |     private function compare_profiles(\Profile $a, \Profile $b) | ||||||
|  |     { | ||||||
|  |         if (($av = $a->getID()) != ($bv = $b->getID())) { | ||||||
|  |             throw new Exception('Compare Profiles 1 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getNickname()) != ($bv = $b->getNickname())) { | ||||||
|  |             throw new Exception('Compare Profiles 2 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getFullname()) != ($bv = $b->getFullname())) { | ||||||
|  |             throw new Exception('Compare Profiles 3 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getUrl()) != ($bv = $b->getUrl())) { | ||||||
|  |             throw new Exception('Compare Profiles 4 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getDescription()) != ($bv = $b->getDescription())) { | ||||||
|  |             throw new Exception('Compare Profiles 5 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getLocation()) != ($bv = $b->getLocation())) { | ||||||
|  |             throw new Exception('Compare Profiles 6 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getNickname()) != ($bv = $b->getNickname())) { | ||||||
|  |             throw new Exception('Compare Profiles 7 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->lat) != ($bv = $b->lat)) { | ||||||
|  |             throw new Exception('Compare Profiles 8 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->lon) != ($bv = $b->lon)) { | ||||||
|  |             throw new Exception('Compare Profiles 9 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private function compare_aprofiles(\Activitypub_profile $a, \Activitypub_profile $b) | ||||||
|  |     { | ||||||
|  |         if (($av = $a->getUri()) != ($bv = $b->getUri())) { | ||||||
|  |             throw new Exception('Compare AProfiles 1 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getUrl()) != ($bv = $b->getUrl())) { | ||||||
|  |             throw new Exception('Compare AProfiles 2 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->getID()) != ($bv = $b->getID())) { | ||||||
|  |             throw new Exception('Compare AProfiles 3 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->profile_id) != ($bv = $b->profile_id)) { | ||||||
|  |             throw new Exception('Compare AProfiles 4 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->inboxuri) != ($bv = $b->inboxuri)) { | ||||||
|  |             throw new Exception('Compare AProfiles 5 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (($av = $a->sharedInboxuri) != ($bv = $b->sharedInboxuri)) { | ||||||
|  |             throw new Exception('Compare AProfiles 6 Fail: $a: '.$av.' is different from $b: '.$bv); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								tests/Unit/ExampleTest.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										18
									
								
								tests/Unit/ExampleTest.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Tests\Unit; | ||||||
|  |  | ||||||
|  | use Tests\TestCase; | ||||||
|  |  | ||||||
|  | class ExampleTest extends TestCase | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * A basic test example. | ||||||
|  |      * | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function testBasicTest() | ||||||
|  |     { | ||||||
|  |         $this->assertTrue(true); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										141
									
								
								tests/Unit/HTTPSignatureTest.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										141
									
								
								tests/Unit/HTTPSignatureTest.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,141 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Tests\Unit; | ||||||
|  |  | ||||||
|  | use Tests\TestCase; | ||||||
|  | use GuzzleHttp\Client; | ||||||
|  | use GuzzleHttp\Handler\CurlHandler; | ||||||
|  | use GuzzleHttp\Handler\MockHandler; | ||||||
|  | use GuzzleHttp\HandlerStack; | ||||||
|  | use GuzzleHttp\Middleware; | ||||||
|  | use GuzzleHttp\Psr7\Request; | ||||||
|  | use GuzzleHttp\Psr7\Response; | ||||||
|  | use HttpSignatures\Context; | ||||||
|  | use HttpSignatures\GuzzleHttpSignatures; | ||||||
|  |  | ||||||
|  | class HTTPSignatureTest extends TestCase | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @var Context | ||||||
|  |      */ | ||||||
|  |     private $context; | ||||||
|  |     /** | ||||||
|  |      * @var Client | ||||||
|  |      */ | ||||||
|  |     private $client; | ||||||
|  |     /** | ||||||
|  |      * @var | ||||||
|  |      */ | ||||||
|  |     private $history = []; | ||||||
|  |  | ||||||
|  |     public function testLibraryInstalled() | ||||||
|  |     { | ||||||
|  |         $this->assertTrue(class_exists('\GuzzleHttp\Client')); | ||||||
|  |         $this->assertTrue(class_exists('\HttpSignatures\Context')); | ||||||
|  |         $this->assertTrue(class_exists('\HttpSignatures\GuzzleHttpSignatures')); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function setUp() | ||||||
|  |     { | ||||||
|  |         $this->context = new Context([ | ||||||
|  |             'keys' => ['pda' => 'secret'], | ||||||
|  |             'algorithm' => 'hmac-sha256', | ||||||
|  |             'headers' => ['(request-target)', 'date'], | ||||||
|  |         ]); | ||||||
|  |         $stack = new HandlerStack(); | ||||||
|  |         $stack->setHandler(new MockHandler([ | ||||||
|  |             new Response(200, ['Content-Length' => 0]), | ||||||
|  |         ])); | ||||||
|  |         $stack->push(GuzzleHttpSignatures::middlewareFromContext($this->context)); | ||||||
|  |         $stack->push(Middleware::history($this->history)); | ||||||
|  |         $this->client = new Client(['handler' => $stack]); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * test signing a message | ||||||
|  |      */ | ||||||
|  |     public function testGuzzleRequestHasExpectedHeaders() | ||||||
|  |     { | ||||||
|  |         $this->client->get('/path?query=123', [ | ||||||
|  |             'headers' => ['date' => 'today', 'accept' => 'llamas'] | ||||||
|  |         ]); | ||||||
|  |         // get last request | ||||||
|  |         $message = end($this->history); | ||||||
|  |         /** @var Request $request */ | ||||||
|  |         $request = $message['request']; | ||||||
|  |         /** @var Response $response */ | ||||||
|  |         $response = $message['request']; | ||||||
|  |         $expectedString = implode( | ||||||
|  |             ',', | ||||||
|  |             [ | ||||||
|  |                 'keyId="pda"', | ||||||
|  |                 'algorithm="hmac-sha256"', | ||||||
|  |                 'headers="(request-target) date"', | ||||||
|  |                 'signature="SFlytCGpsqb/9qYaKCQklGDvwgmrwfIERFnwt+yqPJw="', | ||||||
|  |             ] | ||||||
|  |         ); | ||||||
|  |         $this->assertEquals( | ||||||
|  |             [$expectedString], | ||||||
|  |             $request->getHeader('Signature') | ||||||
|  |         ); | ||||||
|  |         $this->assertEquals( | ||||||
|  |             ['Signature ' . $expectedString], | ||||||
|  |             $request->getHeader('Authorization') | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * test signing a message with a URL that doesn't contain a ?query | ||||||
|  |      */ | ||||||
|  |     public function testGuzzleRequestHasExpectedHeaders2() | ||||||
|  |     { | ||||||
|  |         $this->client->get('/path', [ | ||||||
|  |             'headers' => ['date' => 'today', 'accept' => 'llamas'] | ||||||
|  |         ]); | ||||||
|  |         // get last request | ||||||
|  |         $message = end($this->history); | ||||||
|  |         /** @var Request $request */ | ||||||
|  |         $request = $message['request']; | ||||||
|  |         /** @var Response $response */ | ||||||
|  |         $response = $message['request']; | ||||||
|  |         $expectedString = implode( | ||||||
|  |             ',', | ||||||
|  |             [ | ||||||
|  |                 'keyId="pda"', | ||||||
|  |                 'algorithm="hmac-sha256"', | ||||||
|  |                 'headers="(request-target) date"', | ||||||
|  |                 'signature="DAtF133khP05pS5Gh8f+zF/UF7mVUojMj7iJZO3Xk4o="', | ||||||
|  |             ] | ||||||
|  |         ); | ||||||
|  |         $this->assertEquals( | ||||||
|  |             [$expectedString], | ||||||
|  |             $request->getHeader('Signature') | ||||||
|  |         ); | ||||||
|  |         $this->assertEquals( | ||||||
|  |             ['Signature ' . $expectedString], | ||||||
|  |             $request->getHeader('Authorization') | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     public function getVerifyGuzzleRequestVectors() | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             /* path, headers */ | ||||||
|  |             ['/path?query=123', ['date' => 'today', 'accept' => 'llamas']], | ||||||
|  |             ['/path?z=zebra&a=antelope', ['date' => 'today']], | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * @dataProvider getVerifyGuzzleRequestVectors | ||||||
|  |      * @param string $path | ||||||
|  |      * @param array $headers | ||||||
|  |      */ | ||||||
|  |     public function testVerifyGuzzleRequest($path, $headers) | ||||||
|  |     { | ||||||
|  |         $this->client->get($path, ['headers' => $headers]); | ||||||
|  |         // get last request | ||||||
|  |         $message = end($this->history); | ||||||
|  |         /** @var Request $request */ | ||||||
|  |         $request = $message['request']; | ||||||
|  |         /** @var Response $response */ | ||||||
|  |         $response = $message['request']; | ||||||
|  |         $this->assertTrue($this->context->verifier()->isValid($request)); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										116
									
								
								utils/AcceptHeader.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								utils/AcceptHeader.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Note : Code is released under the GNU LGPL | ||||||
|  |  * | ||||||
|  |  * Please do not change the header of this file | ||||||
|  |  * | ||||||
|  |  * This library is free software; you can redistribute it and/or modify it under the terms of the GNU | ||||||
|  |  * Lesser General Public License as published by the Free Software Foundation; either version 2 of | ||||||
|  |  * the License, or (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | ||||||
|  |  * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||||||
|  |  * | ||||||
|  |  * See the GNU Lesser General Public License for more details. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The AcceptHeader page will parse and sort the different | ||||||
|  |  * allowed types for the content negociations | ||||||
|  |  * | ||||||
|  |  * @author Pierrick Charron <pierrick@webstart.fr> | ||||||
|  |  */ | ||||||
|  | class AcceptHeader extends \ArrayObject | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Constructor | ||||||
|  |      * | ||||||
|  |      * @param string $header Value of the Accept header | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function __construct($header) | ||||||
|  |     { | ||||||
|  |         $acceptedTypes = $this->_parse($header); | ||||||
|  |         usort($acceptedTypes, [$this, '_compare']); | ||||||
|  |         parent::__construct($acceptedTypes); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Parse the accept header and return an array containing | ||||||
|  |      * all the informations about the Accepted types | ||||||
|  |      * | ||||||
|  |      * @param string $header Value of the Accept header | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     private function _parse($data) | ||||||
|  |     { | ||||||
|  |         $array = []; | ||||||
|  |         $items = explode(',', $data); | ||||||
|  |         foreach ($items as $item) { | ||||||
|  |             $elems = explode(';', $item); | ||||||
|  |  | ||||||
|  |             $acceptElement = []; | ||||||
|  |             $mime = current($elems); | ||||||
|  |             list($type, $subtype) = explode('/', $mime); | ||||||
|  |             $acceptElement['type'] = trim($type); | ||||||
|  |             $acceptElement['subtype'] = trim($subtype); | ||||||
|  |             $acceptElement['raw'] = $mime; | ||||||
|  |  | ||||||
|  |             $acceptElement['params'] = []; | ||||||
|  |             while (next($elems)) { | ||||||
|  |                 list($name, $value) = explode('=', current($elems)); | ||||||
|  |                 $acceptElement['params'][trim($name)] = trim($value); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             $array[] = $acceptElement; | ||||||
|  |         } | ||||||
|  |         return $array; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Compare two Accepted types with their parameters to know | ||||||
|  |      * if one media type should be used instead of an other | ||||||
|  |      * | ||||||
|  |      * @param array $a The first media type and its parameters | ||||||
|  |      * @param array $b The second media type and its parameters | ||||||
|  |      * @return int | ||||||
|  |      */ | ||||||
|  |     private function _compare($a, $b) | ||||||
|  |     { | ||||||
|  |         $a_q = isset($a['params']['q']) ? floatval($a['params']['q']) : 1.0; | ||||||
|  |         $b_q = isset($b['params']['q']) ? floatval($b['params']['q']) : 1.0; | ||||||
|  |         if ($a_q === $b_q) { | ||||||
|  |             $a_count = count($a['params']); | ||||||
|  |             $b_count = count($b['params']); | ||||||
|  |             if ($a_count === $b_count) { | ||||||
|  |                 if ($r = $this->_compareSubType($a['subtype'], $b['subtype'])) { | ||||||
|  |                     return $r; | ||||||
|  |                 } else { | ||||||
|  |                     return $this->_compareSubType($a['type'], $b['type']); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 return $a_count < $b_count; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             return $a_q < $b_q; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Compare two subtypes | ||||||
|  |      * | ||||||
|  |      * @param string $a First subtype to compare | ||||||
|  |      * @param string $b Second subtype to compare | ||||||
|  |      * @return int | ||||||
|  |      */ | ||||||
|  |     private function _compareSubType($a, $b) | ||||||
|  |     { | ||||||
|  |         if ($a === '*' && $b !== '*') { | ||||||
|  |             return 1; | ||||||
|  |         } elseif ($b === '*' && $a !== '*') { | ||||||
|  |             return -1; | ||||||
|  |         } else { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										101
									
								
								utils/Activitypub_activityverb2.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								utils/Activitypub_activityverb2.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Utility class to hold a bunch of constant defining default verb types | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_activityverb2 extends Managed_DataObject | ||||||
|  | { | ||||||
|  |     const FULL_LIST = | ||||||
|  |     [ | ||||||
|  |         'Accept'          => 'https://www.w3.org/ns/activitystreams#Accept', | ||||||
|  |         'TentativeAccept' => 'https://www.w3.org/ns/activitystreams#TentativeAccept', | ||||||
|  |         'Add'             => 'https://www.w3.org/ns/activitystreams#Add', | ||||||
|  |         'Arrive'          => 'https://www.w3.org/ns/activitystreams#Arrive', | ||||||
|  |         'Create'          => 'https://www.w3.org/ns/activitystreams#Create', | ||||||
|  |         'Delete'          => 'https://www.w3.org/ns/activitystreams#Delete', | ||||||
|  |         'Follow'          => 'https://www.w3.org/ns/activitystreams#Follow', | ||||||
|  |         'Ignore'          => 'https://www.w3.org/ns/activitystreams#Ignore', | ||||||
|  |         'Join'            => 'https://www.w3.org/ns/activitystreams#Join', | ||||||
|  |         'Leave'           => 'https://www.w3.org/ns/activitystreams#Leave', | ||||||
|  |         'Like'            => 'https://www.w3.org/ns/activitystreams#Like', | ||||||
|  |         'Offer'           => 'https://www.w3.org/ns/activitystreams#Offer', | ||||||
|  |         'Invite'          => 'https://www.w3.org/ns/activitystreams#Invite', | ||||||
|  |         'Reject'          => 'https://www.w3.org/ns/activitystreams#Reject', | ||||||
|  |         'TentativeReject' => 'https://www.w3.org/ns/activitystreams#TentativeReject', | ||||||
|  |         'Remove'          => 'https://www.w3.org/ns/activitystreams#Remove', | ||||||
|  |         'Undo'            => 'https://www.w3.org/ns/activitystreams#Undo', | ||||||
|  |         'Update'          => 'https://www.w3.org/ns/activitystreams#Update', | ||||||
|  |         'View'            => 'https://www.w3.org/ns/activitystreams#View', | ||||||
|  |         'Listen'          => 'https://www.w3.org/ns/activitystreams#Listen', | ||||||
|  |         'Read'            => 'https://www.w3.org/ns/activitystreams#Read', | ||||||
|  |         'Move'            => 'https://www.w3.org/ns/activitystreams#Move', | ||||||
|  |         'Travel'          => 'https://www.w3.org/ns/activitystreams#Travel', | ||||||
|  |         'Announce'        => 'https://www.w3.org/ns/activitystreams#Announce', | ||||||
|  |         'Block'           => 'https://www.w3.org/ns/activitystreams#Block', | ||||||
|  |         'Flag'            => 'https://www.w3.org/ns/activitystreams#Flag', | ||||||
|  |         'Dislike'         => 'https://www.w3.org/ns/activitystreams#Dislike', | ||||||
|  |         'Question'        => 'https://www.w3.org/ns/activitystreams#Question' | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     const KNOWN = | ||||||
|  |     [ | ||||||
|  |         'Accept', | ||||||
|  |         'Create', | ||||||
|  |         'Delete', | ||||||
|  |         'Follow', | ||||||
|  |         'Like', | ||||||
|  |         'Undo', | ||||||
|  |         'Announce' | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Converts canonical into verb. | ||||||
|  |      * | ||||||
|  |      * @author GNU Social | ||||||
|  |      * @param string $verb | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public static function canonical($verb) | ||||||
|  |     { | ||||||
|  |         $ns = 'https://www.w3.org/ns/activitystreams#'; | ||||||
|  |         if (substr($verb, 0, mb_strlen($ns)) == $ns) { | ||||||
|  |             return substr($verb, mb_strlen($ns)); | ||||||
|  |         } else { | ||||||
|  |             return $verb; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										157
									
								
								utils/discoveryhints.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										157
									
								
								utils/discoveryhints.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,157 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * Some utilities for generating hint data | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    GNUsocial | ||||||
|  |  * @copyright 2010 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class DiscoveryHints | ||||||
|  | { | ||||||
|  |     public static function fromXRD(XML_XRD $xrd) | ||||||
|  |     { | ||||||
|  |         $hints = []; | ||||||
|  |  | ||||||
|  |         if (Event::handle('StartDiscoveryHintsFromXRD', [$xrd, &$hints])) { | ||||||
|  |             foreach ($xrd->links as $link) { | ||||||
|  |                 switch ($link->rel) { | ||||||
|  |                 case WebFingerResource_Profile::PROFILEPAGE: | ||||||
|  |                     $hints['profileurl'] = $link->href; | ||||||
|  |                     break; | ||||||
|  |                     $hints['salmon'] = $link->href; | ||||||
|  |                     break; | ||||||
|  |                 case Discovery::UPDATESFROM: | ||||||
|  |                     if (empty($link->type) || $link->type == 'application/atom+xml') { | ||||||
|  |                         $hints['feedurl'] = $link->href; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case Discovery::HCARD: | ||||||
|  |                 case Discovery::MF2_HCARD: | ||||||
|  |                     $hints['hcard'] = $link->href; | ||||||
|  |                     break; | ||||||
|  |                 default: | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Event::handle('EndDiscoveryHintsFromXRD', [$xrd, &$hints]); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $hints; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static function fromHcardUrl($url) | ||||||
|  |     { | ||||||
|  |         $client = new HTTPClient(); | ||||||
|  |         $client->setHeader('Accept', 'text/html,application/xhtml+xml'); | ||||||
|  |         try { | ||||||
|  |             $response = $client->get($url); | ||||||
|  |  | ||||||
|  |             if (!$response->isOk()) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         } catch (HTTP_Request2_Exception $e) { | ||||||
|  |             // Any HTTPClient error that might've been thrown | ||||||
|  |             common_log(LOG_ERR, __METHOD__ . ':'.$e->getMessage()); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return self::hcardHints( | ||||||
|  |             $response->getBody(), | ||||||
|  |                                 $response->getEffectiveUrl() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static function hcardHints($body, $url) | ||||||
|  |     { | ||||||
|  |         $hcard = self::_hcard($body, $url); | ||||||
|  |  | ||||||
|  |         if (empty($hcard)) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $hints = []; | ||||||
|  |  | ||||||
|  |         // XXX: don't copy stuff into an array and then copy it again | ||||||
|  |  | ||||||
|  |         if (array_key_exists('nickname', $hcard) && !empty($hcard['nickname'][0])) { | ||||||
|  |             $hints['nickname'] = $hcard['nickname'][0]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (array_key_exists('name', $hcard) && !empty($hcard['name'][0])) { | ||||||
|  |             $hints['fullname'] = $hcard['name'][0]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (array_key_exists('photo', $hcard) && count($hcard['photo'])) { | ||||||
|  |             $hints['avatar'] = $hcard['photo'][0]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (array_key_exists('note', $hcard) && !empty($hcard['note'][0])) { | ||||||
|  |             $hints['bio'] = $hcard['note'][0]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (array_key_exists('adr', $hcard) && !empty($hcard['adr'][0])) { | ||||||
|  |             $hints['location'] = $hcard['adr'][0]['value']; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (array_key_exists('url', $hcard) && !empty($hcard['url'][0])) { | ||||||
|  |             $hints['homepage'] = $hcard['url'][0]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $hints; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static function _hcard($body, $url) | ||||||
|  |     { | ||||||
|  |         $mf2 = new Mf2\Parser($body, $url); | ||||||
|  |         $mf2 = $mf2->parse(); | ||||||
|  |  | ||||||
|  |         if (empty($mf2['items'])) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $hcards = []; | ||||||
|  |  | ||||||
|  |         foreach ($mf2['items'] as $item) { | ||||||
|  |             if (!in_array('h-card', $item['type'])) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // We found a match, return it immediately | ||||||
|  |             if (isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) { | ||||||
|  |                 return $item['properties']; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Let's keep all the hcards for later, to return one of them at least | ||||||
|  |             $hcards[] = $item['properties']; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // No match immediately for the url we expected, but there were h-cards found | ||||||
|  |         if (count($hcards) > 0) { | ||||||
|  |             return $hcards[0]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										555
									
								
								utils/explorer.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										555
									
								
								utils/explorer.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,19 +20,18 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * ActivityPub's own Explorer |  * ActivityPub's own Explorer | ||||||
|  * |  * | ||||||
|  * Allows to discovery new (or the same) ActivityPub profiles |  * Allows to discovery new (or the same) Profiles (both local or remote) | ||||||
|  * |  * | ||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
| @@ -42,172 +41,424 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  */ |  */ | ||||||
| class Activitypub_explorer | class Activitypub_explorer | ||||||
| { | { | ||||||
|         private $discovered_actor_profiles = array (); |     private $discovered_actor_profiles = []; | ||||||
|  |  | ||||||
|         /** |     /** | ||||||
|          * Get every profile from the given URL |      * Shortcut function to get a single profile from its URL. | ||||||
|          * This function cleans the $this->discovered_actor_profiles array |      * | ||||||
|          * so that there is no erroneous data |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          * |      * @param string $url | ||||||
|          * @param string $url User's url |      * @return Profile | ||||||
|          * @return array of Profile objects |      * @throws Exception | ||||||
|          */ |      */ | ||||||
|         public function lookup ($url) |     public static function get_profile_from_url($url) | ||||||
|         { |     { | ||||||
|                 $this->discovered_actor_profiles = array (); |         $discovery = new Activitypub_explorer; | ||||||
|  |         // Get valid Actor object | ||||||
|  |         $actor_profile = $discovery->lookup($url); | ||||||
|  |         if (!empty($actor_profile)) { | ||||||
|  |             return $actor_profile[0]; | ||||||
|  |         } | ||||||
|  |         throw new Exception('Invalid Actor.'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|                 return $this->_lookup ($url); |     /** | ||||||
|  |      * Get every profile from the given URL | ||||||
|  |      * This function cleans the $this->discovered_actor_profiles array | ||||||
|  |      * so that there is no erroneous data | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $url User's url | ||||||
|  |      * @return array of Profile objects | ||||||
|  |      */ | ||||||
|  |     public function lookup($url) | ||||||
|  |     { | ||||||
|  |         if (in_array($url, ACTIVITYPUB_PUBLIC_TO)) { | ||||||
|  |             return []; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         common_debug('ActivityPub Explorer: Started now looking for '.$url); | ||||||
|          * Get every profile from the given URL |         $this->discovered_actor_profiles = []; | ||||||
|          * This is a recursive function that will accumulate the results on |  | ||||||
|          * $discovered_actor_profiles array |  | ||||||
|          * |  | ||||||
|          * @param string $url User's url |  | ||||||
|          * @return array of Profile objects |  | ||||||
|          */ |  | ||||||
|         private function _lookup ($url) |  | ||||||
|         { |  | ||||||
|                 // First check if we already have it locally and, if so, return it |  | ||||||
|                 // If the local fetch fails: grab it remotely, store locally and return |  | ||||||
|                 if (! ($this->grab_local_user ($url) || $this->grab_remote_user ($url))) { |  | ||||||
|                     throw new Exception ("User not found"); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|  |         return $this->_lookup($url); | ||||||
|  |     } | ||||||
|  |  | ||||||
|                 return $this->discovered_actor_profiles; |     /** | ||||||
|  |      * Get every profile from the given URL | ||||||
|  |      * This is a recursive function that will accumulate the results on | ||||||
|  |      * $discovered_actor_profiles array | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $url User's url | ||||||
|  |      * @return array of Profile objects | ||||||
|  |      */ | ||||||
|  |     private function _lookup($url) | ||||||
|  |     { | ||||||
|  |         // First check if we already have it locally and, if so, return it | ||||||
|  |         // If the local fetch fails: grab it remotely, store locally and return | ||||||
|  |         if (! ($this->grab_local_user($url) || $this->grab_remote_user($url))) { | ||||||
|  |             throw new Exception('User not found.'); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         return $this->discovered_actor_profiles; | ||||||
|          * Get a local user profiles from its URL and joins it on |     } | ||||||
|          * $this->discovered_actor_profiles |  | ||||||
|          * |  | ||||||
|          * @param string $url User's url |  | ||||||
|          * @return boolean success state |  | ||||||
|          */ |  | ||||||
|         private function grab_local_user ($url) |  | ||||||
|         { |  | ||||||
|                 if (($actor_profile = self::get_profile_by_url ($url)) != false) { |  | ||||||
|                         $this->discovered_actor_profiles[]= $actor_profile; |  | ||||||
|                         return true; |  | ||||||
|                 } else { |  | ||||||
|                         /******************************** XXX: ******************************** |  | ||||||
|                          * Sometimes it is not true that the user is not locally available,   * |  | ||||||
|                          * mostly when it is a local user and URLs slightly changed           * |  | ||||||
|                          * e.g.: GS instance owner changed from standard urls to pretty urls  * |  | ||||||
|                          * (not sure if this is necessary, but anyway)                        * |  | ||||||
|                          **********************************************************************/ |  | ||||||
|  |  | ||||||
|                         // Iff we really are in the same instance |     /** | ||||||
|                         $root_url_len = strlen (common_root_url ()); |      * This ensures that we are using a valid ActivityPub URI | ||||||
|                         if (substr ($url, 0, $root_url_len) == common_root_url ()) { |      * | ||||||
|                                 // Grab the nickname and try to get the user |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|                                 if (($actor_profile = Profile::getKV ("nickname", substr ($url, $root_url_len))) != false) { |      * @param string $url | ||||||
|                                         $this->discovered_actor_profiles[]= $actor_profile; |      * @return boolean success state (related to the response) | ||||||
|                                         return true; |      * @throws Exception (If the HTTP request fails) | ||||||
|                                 } |      */ | ||||||
|                         } |     private function ensure_proper_remote_uri($url) | ||||||
|  |     { | ||||||
|  |         $client    = new HTTPClient(); | ||||||
|  |         $headers   = []; | ||||||
|  |         $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; | ||||||
|  |         $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; | ||||||
|  |         $response  = $client->get($url, $headers); | ||||||
|  |         $res = json_decode($response->getBody(), true); | ||||||
|  |         if (self::validate_remote_response($res)) { | ||||||
|  |             $this->temp_res = $res; | ||||||
|  |             return true; | ||||||
|  |         } else { | ||||||
|  |             common_debug('ActivityPub Explorer: Invalid potential remote actor while ensuring URI: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Get a local user profile from its URL and joins it on | ||||||
|  |      * $this->discovered_actor_profiles | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $uri Actor's uri | ||||||
|  |      * @return boolean success state | ||||||
|  |      */ | ||||||
|  |     private function grab_local_user($uri, $online = false) | ||||||
|  |     { | ||||||
|  |         if ($online) { | ||||||
|  |             common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' with online resources.'); | ||||||
|  |         } else { | ||||||
|  |             common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' offline.'); | ||||||
|  |         } | ||||||
|  |         // Ensure proper remote URI | ||||||
|  |         // If an exception occurs here it's better to just leave everything | ||||||
|  |         // break than to continue processing | ||||||
|  |         if ($online && $this->ensure_proper_remote_uri($uri)) { | ||||||
|  |             $uri = $this->temp_res["id"]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Try standard ActivityPub route | ||||||
|  |         // Is this a known filthy little mudblood? | ||||||
|  |         $aprofile = self::get_aprofile_by_url($uri); | ||||||
|  |         if ($aprofile instanceof Activitypub_profile) { | ||||||
|  |             $profile = $aprofile->local_profile(); | ||||||
|  |             common_debug('ActivityPub Explorer: Found a local Aprofile for '.$uri); | ||||||
|  |             // We found something! | ||||||
|  |             $this->discovered_actor_profiles[]= $profile; | ||||||
|  |             unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system | ||||||
|  |             return true; | ||||||
|  |         } else { | ||||||
|  |             common_debug('ActivityPub Explorer: Unable to find a local Aprofile for '.$uri.' - looking for a Profile instead.'); | ||||||
|  |             // Well, maybe it is a pure blood? | ||||||
|  |             // Iff, we are in the same instance: | ||||||
|  |             $ACTIVITYPUB_BASE_ACTOR_URI_length = strlen(ACTIVITYPUB_BASE_ACTOR_URI); | ||||||
|  |             if (substr($uri, 0, $ACTIVITYPUB_BASE_ACTOR_URI_length) == ACTIVITYPUB_BASE_ACTOR_URI) { | ||||||
|  |                 try { | ||||||
|  |                     $profile = Profile::getByID(intval(substr($uri, $ACTIVITYPUB_BASE_ACTOR_URI_length))); | ||||||
|  |                     common_debug('ActivityPub Explorer: Found a Profile for '.$uri); | ||||||
|  |                     // We found something! | ||||||
|  |                     $this->discovered_actor_profiles[]= $profile; | ||||||
|  |                     unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system | ||||||
|  |                     return true; | ||||||
|  |                 } catch (Exception $e) { | ||||||
|  |                     // Let the exception go on its merry way. | ||||||
|  |                     common_debug('ActivityPub Explorer: Unable to find a Profile for '.$uri); | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // If offline grabbing failed, attempt again with online resources | ||||||
|  |         if (!$online) { | ||||||
|  |             common_debug('ActivityPub Explorer: Will try everything again with online resources against: '.$uri); | ||||||
|  |             return $this->grab_local_user($uri, true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Get a remote user(s) profile(s) from its URL and joins it on | ||||||
|  |      * $this->discovered_actor_profiles | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $url User's url | ||||||
|  |      * @return boolean success state | ||||||
|  |      */ | ||||||
|  |     private function grab_remote_user($url) | ||||||
|  |     { | ||||||
|  |         common_debug('ActivityPub Explorer: Trying to grab a remote actor for '.$url); | ||||||
|  |         if (!isset($this->temp_res)) { | ||||||
|  |             $client    = new HTTPClient(); | ||||||
|  |             $headers   = []; | ||||||
|  |             $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; | ||||||
|  |             $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; | ||||||
|  |             $response  = $client->get($url, $headers); | ||||||
|  |             $res = json_decode($response->getBody(), true); | ||||||
|  |         } else { | ||||||
|  |             $res = $this->temp_res; | ||||||
|  |             unset($this->temp_res); | ||||||
|  |         } | ||||||
|  |         if (isset($res['type']) && $res['type'] === 'OrderedCollection' && isset($res['first'])) { // It's a potential collection of actors!!! | ||||||
|  |             common_debug('ActivityPub Explorer: Found a collection of actors for '.$url); | ||||||
|  |             $this->travel_collection($res['first']); | ||||||
|  |             return true; | ||||||
|  |         } elseif (self::validate_remote_response($res)) { | ||||||
|  |             common_debug('ActivityPub Explorer: Found a valid remote actor for '.$url); | ||||||
|  |             $this->discovered_actor_profiles[]= $this->store_profile($res); | ||||||
|  |             return true; | ||||||
|  |         } else { | ||||||
|  |             common_debug('ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // TODO: Fallback to OStatus | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Save remote user profile in local instance | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $res remote response | ||||||
|  |      * @return Profile remote Profile object | ||||||
|  |      */ | ||||||
|  |     private function store_profile($res) | ||||||
|  |     { | ||||||
|  |         // ActivityPub Profile | ||||||
|  |         $aprofile                 = new Activitypub_profile; | ||||||
|  |         $aprofile->uri            = $res['id']; | ||||||
|  |         $aprofile->nickname       = $res['preferredUsername']; | ||||||
|  |         $aprofile->fullname       = isset($res['name']) ? $res['name'] : null; | ||||||
|  |         $aprofile->bio            = isset($res['summary']) ? substr(strip_tags($res['summary']), 0, 1000) : null; | ||||||
|  |         $aprofile->inboxuri       = $res['inbox']; | ||||||
|  |         $aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']; | ||||||
|  |  | ||||||
|  |         $aprofile->do_insert(); | ||||||
|  |         $profile = $aprofile->local_profile(); | ||||||
|  |  | ||||||
|  |         // Public Key | ||||||
|  |         $apRSA = new Activitypub_rsa(); | ||||||
|  |         $apRSA->profile_id = $profile->getID(); | ||||||
|  |         $apRSA->public_key = $res['publicKey']['publicKeyPem']; | ||||||
|  |         $apRSA->store_keys(); | ||||||
|  |  | ||||||
|  |         // Avatar | ||||||
|  |         if (isset($res['icon']['url'])) { | ||||||
|  |             try { | ||||||
|  |                 $this->update_avatar($profile, $res['icon']['url']); | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 // Let the exception go, it isn't a serious issue | ||||||
|  |                 common_debug('ActivityPub Explorer: An error ocurred while grabbing remote avatar: '.$e->getMessage()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $profile; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Download and update given avatar image | ||||||
|  |      * | ||||||
|  |      * @author GNU Social | ||||||
|  |      * @param  Profile $profile | ||||||
|  |      * @param  string $url | ||||||
|  |      * @return Avatar    The Avatar we have on disk. | ||||||
|  |      * @throws Exception in various failure cases | ||||||
|  |      */ | ||||||
|  |     public static function update_avatar(Profile $profile, $url) | ||||||
|  |     { | ||||||
|  |         common_debug('ActivityPub Explorer: Started grabbing remote avatar from: '.$url); | ||||||
|  |         if (!filter_var($url, FILTER_VALIDATE_URL)) { | ||||||
|  |             // TRANS: Server exception. %s is a URL. | ||||||
|  |             common_debug('ActivityPub Explorer: Failed because it is an invalid url: '.$url); | ||||||
|  |             throw new ServerException(sprintf('Invalid avatar URL %s.'), $url); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // @todo FIXME: This should be better encapsulated | ||||||
|  |         // ripped from oauthstore.php (for old OMB client) | ||||||
|  |         $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); | ||||||
|  |         try { | ||||||
|  |             $imgData = HTTPClient::quickGet($url); | ||||||
|  |             // Make sure it's at least an image file. ImageFile can do the rest. | ||||||
|  |             if (false === getimagesizefromstring($imgData)) { | ||||||
|  |                 common_debug('ActivityPub Explorer: Failed because the downloaded avatar: '.$url. 'is not a valid image.'); | ||||||
|  |                 throw new UnsupportedMediaException('Downloaded avatar was not an image.'); | ||||||
|  |             } | ||||||
|  |             file_put_contents($temp_filename, $imgData); | ||||||
|  |             unset($imgData);    // No need to carry this in memory. | ||||||
|  |             common_debug('ActivityPub Explorer: Stored dowloaded avatar in: '.$temp_filename); | ||||||
|  |  | ||||||
|  |             $id = $profile->getID(); | ||||||
|  |  | ||||||
|  |             $imagefile = new ImageFile(null, $temp_filename); | ||||||
|  |             $filename = Avatar::filename( | ||||||
|  |                 $id, | ||||||
|  |                 image_type_to_extension($imagefile->type), | ||||||
|  |                 null, | ||||||
|  |                 common_timestamp() | ||||||
|  |             ); | ||||||
|  |             rename($temp_filename, Avatar::path($filename)); | ||||||
|  |             common_debug('ActivityPub Explorer: Moved avatar from: '.$temp_filename.' to '.$filename); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             common_debug('ActivityPub Explorer: Something went wrong while processing the avatar from: '.$url.' details: '.$e->getMessage()); | ||||||
|  |             unlink($temp_filename); | ||||||
|  |             throw $e; | ||||||
|  |         } | ||||||
|  |         // @todo FIXME: Hardcoded chmod is lame, but seems to be necessary to | ||||||
|  |         // keep from accidentally saving images from command-line (queues) | ||||||
|  |         // that can't be read from web server, which causes hard-to-notice | ||||||
|  |         // problems later on: | ||||||
|  |         // | ||||||
|  |         // http://status.net/open-source/issues/2663 | ||||||
|  |         chmod(Avatar::path($filename), 0644); | ||||||
|  |  | ||||||
|  |         $profile->setOriginal($filename); | ||||||
|  |  | ||||||
|  |         $orig = clone($profile); | ||||||
|  |         $profile->avatar = $url; | ||||||
|  |         $profile->update($orig); | ||||||
|  |  | ||||||
|  |         common_debug('ActivityPub Explorer: Seted Avatar from: '.$url.' to profile.'); | ||||||
|  |         return Avatar::getUploaded($profile); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Validates a remote response in order to determine whether this | ||||||
|  |      * response is a valid profile or not | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param array $res remote response | ||||||
|  |      * @return boolean success state | ||||||
|  |      */ | ||||||
|  |     public static function validate_remote_response($res) | ||||||
|  |     { | ||||||
|  |         if (!isset($res['id'], $res['preferredUsername'], $res['inbox'], $res['publicKey']['publicKeyPem'])) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Get a ActivityPub Profile from it's uri | ||||||
|  |      * Unfortunately GNU Social cache is not truly reliable when handling | ||||||
|  |      * potential ActivityPub remote profiles, as so it is important to use | ||||||
|  |      * this hacky workaround (at least for now) | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $v URL | ||||||
|  |      * @return boolean|Activitypub_profile false if fails | Aprofile object if successful | ||||||
|  |      */ | ||||||
|  |     public static function get_aprofile_by_url($v) | ||||||
|  |     { | ||||||
|  |         $i = Managed_DataObject::getcached("Activitypub_profile", "uri", $v); | ||||||
|  |         if (empty($i)) { // false = cache miss | ||||||
|  |             $i = new Activitypub_profile; | ||||||
|  |             $result = $i->get("uri", $v); | ||||||
|  |             if ($result) { | ||||||
|  |                 // Hit! | ||||||
|  |                 $i->encache(); | ||||||
|  |             } else { | ||||||
|                 return false; |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $i; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Given a valid actor profile url returns its inboxes | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $url of Actor profile | ||||||
|  |      * @return boolean|array false if fails | array with inbox and shared inbox if successful | ||||||
|  |      */ | ||||||
|  |     public static function get_actor_inboxes_uri($url) | ||||||
|  |     { | ||||||
|  |         $client    = new HTTPClient(); | ||||||
|  |         $headers   = []; | ||||||
|  |         $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; | ||||||
|  |         $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; | ||||||
|  |         $response  = $client->get($url, $headers); | ||||||
|  |         if (!$response->isOk()) { | ||||||
|  |             throw new Exception('Invalid Actor URL.'); | ||||||
|  |         } | ||||||
|  |         $res = json_decode($response->getBody(), true); | ||||||
|  |         if (self::validate_remote_response($res)) { | ||||||
|  |             return [ | ||||||
|  |                 'inbox' => $res['inbox'], | ||||||
|  |                 'sharedInbox' => isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox'] | ||||||
|  |             ]; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         return false; | ||||||
|          * Get a remote user(s) profile(s) from its URL and joins it on |     } | ||||||
|          * $this->discovered_actor_profiles |  | ||||||
|          * |  | ||||||
|          * @param string $url User's url |  | ||||||
|          * @return boolean success state |  | ||||||
|          */ |  | ||||||
|         private function grab_remote_user ($url) |  | ||||||
|         { |  | ||||||
|                 $client    = new HTTPClient (); |  | ||||||
|                 $headers   = array(); |  | ||||||
|                 $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; |  | ||||||
|                 $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; |  | ||||||
|                 $response  = $client->get ($url, $headers); |  | ||||||
|                 if (!$response->isOk ()) { |  | ||||||
|                     throw new Exception ("Invalid Actor URL."); |  | ||||||
|                 } |  | ||||||
|                 $res = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); |  | ||||||
|                 if (isset ($res["orderedItems"])) { // It's a potential collection of actors!!! |  | ||||||
|                         foreach ($res["orderedItems"] as $profile) { |  | ||||||
|                                 if ($this->_lookup ($profile) == false) { |  | ||||||
|                                         // XXX: Invalid actor found, not sure how we handle those |  | ||||||
|                                 } |  | ||||||
|                         } |  | ||||||
|                         // Go through entire collection |  | ||||||
|                         if (!is_null ($res["next"])) { |  | ||||||
|                                 $this->_lookup ($res["next"]); |  | ||||||
|                         } |  | ||||||
|                         return true; |  | ||||||
|                 } else if ($this->validate_remote_response ($res)) { |  | ||||||
|                         $this->discovered_actor_profiles[]= $this->store_profile ($res); |  | ||||||
|                         return true; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return false; |     /** | ||||||
|  |      * Allows the Explorer to transverse a collection of persons. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param type $url | ||||||
|  |      * @return boolean | ||||||
|  |      */ | ||||||
|  |     private function travel_collection($url) | ||||||
|  |     { | ||||||
|  |         $client    = new HTTPClient(); | ||||||
|  |         $headers   = []; | ||||||
|  |         $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; | ||||||
|  |         $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; | ||||||
|  |         $response  = $client->get($url, $headers); | ||||||
|  |         $res = json_decode($response->getBody(), true); | ||||||
|  |  | ||||||
|  |         if (!isset($res['orderedItems'])) { | ||||||
|  |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         foreach ($res["orderedItems"] as $profile) { | ||||||
|          * Save remote user profile in local instance |             if ($this->_lookup($profile) == false) { | ||||||
|          * |                 common_debug('ActivityPub Explorer: Found an invalid actor for '.$profile); | ||||||
|          * @param array $res remote response |                 // TODO: Invalid actor found, fallback to OStatus | ||||||
|          * @return Profile remote Profile object |             } | ||||||
|          */ |         } | ||||||
|         private function store_profile ($res) |         // Go through entire collection | ||||||
|         { |         if (!is_null($res["next"])) { | ||||||
|                 $aprofile                 = new Activitypub_profile; |             $this->_lookup($res["next"]); | ||||||
|                 $aprofile->uri            = $res["url"]; |  | ||||||
|                 $aprofile->nickname       = $res["nickname"]; |  | ||||||
|                 $aprofile->fullname       = $res["display_name"]; |  | ||||||
|                 $aprofile->bio            = substr ($res["summary"], 0, 1000); |  | ||||||
|                 $aprofile->inboxuri       = $res["inbox"]; |  | ||||||
|                 $aprofile->sharedInboxuri = $res["sharedInbox"]; |  | ||||||
|  |  | ||||||
|                 $aprofile->doInsert (); |  | ||||||
|  |  | ||||||
|                 return $aprofile->localProfile (); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         return true; | ||||||
|          * Validates a remote response in order to determine whether this |     } | ||||||
|          * response is a valid profile or not |  | ||||||
|          * |  | ||||||
|          * @param array $res remote response |  | ||||||
|          * @return boolean success state |  | ||||||
|          */ |  | ||||||
|         private function validate_remote_response ($res) |  | ||||||
|         { |  | ||||||
|                 if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"], $res["inbox"], $res["sharedInbox"])) { |  | ||||||
|                         return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 return true; |     /** | ||||||
|         } |      * Get a remote user array from its URL (this function is only used for | ||||||
|  |      * profile updating and shall not be used for anything else) | ||||||
|         /** |      * | ||||||
|          * Get a profile from it's profileurl |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          * Unfortunately GNU Social cache is not truly reliable when handling |      * @param string $url User's url | ||||||
|          * potential ActivityPub remote profiles, as so it is important to use |      * @throws Exception | ||||||
|          * this hacky workaround (at least for now) |      */ | ||||||
|          * |     public static function get_remote_user_activity($url) | ||||||
|          * @param string $v URL |     { | ||||||
|          * @return boolean|Profile false if fails | Profile object if successful |         $client    = new HTTPClient(); | ||||||
|          */ |         $headers   = []; | ||||||
|         static function get_profile_by_url ($v) |         $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; | ||||||
|         { |         $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; | ||||||
|                 $i = Managed_DataObject::getcached(Profile, "profileurl", $v); |         $response  = $client->get($url, $headers); | ||||||
|                 if (empty ($i)) { // false = cache miss |         $res = json_decode($response->getBody(), true); | ||||||
|                         $i = new Profile; |         if (Activitypub_explorer::validate_remote_response($res)) { | ||||||
|                         $result = $i->get ("profileurl", $v); |             common_debug('ActivityPub Explorer: Found a valid remote actor for '.$url); | ||||||
|                         if ($result) { |             return $res; | ||||||
|                                 // Hit! |  | ||||||
|                                 $i->encache(); |  | ||||||
|                         } else { |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
|                 } |  | ||||||
|                 return $i; |  | ||||||
|         } |         } | ||||||
|  |         throw new Exception('ActivityPub Explorer: Failed to get activity.'); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										300
									
								
								utils/inbox_handler.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								utils/inbox_handler.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | |||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * GNU social - a federating social network | ||||||
|  |  * | ||||||
|  |  * ActivityPubPlugin implementation for GNU Social | ||||||
|  |  * | ||||||
|  |  * LICENCE: This program is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU Affero General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU Affero General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU Affero General Public License | ||||||
|  |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      https://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | if (!defined('GNUSOCIAL')) { | ||||||
|  |     exit(1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActivityPub Inbox Handler | ||||||
|  |  * | ||||||
|  |  * @category  Plugin | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  |  * @link      http://www.gnu.org/software/social/ | ||||||
|  |  */ | ||||||
|  | class Activitypub_inbox_handler | ||||||
|  | { | ||||||
|  |     private $activity; | ||||||
|  |     private $actor; | ||||||
|  |     private $object; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Create a Inbox Handler to receive something from someone. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Array $activity Activity we are receiving | ||||||
|  |      * @param Profile $actor_profile Actor originating the activity | ||||||
|  |      */ | ||||||
|  |     public function __construct($activity, $actor_profile = null) | ||||||
|  |     { | ||||||
|  |         $this->activity = $activity; | ||||||
|  |         $this->object = $activity['object']; | ||||||
|  |  | ||||||
|  |         // Validate Activity | ||||||
|  |         $this->validate_activity(); | ||||||
|  |  | ||||||
|  |         // Get Actor's Profile | ||||||
|  |         if (!is_null($actor_profile)) { | ||||||
|  |             $this->actor = $actor_profile; | ||||||
|  |         } else { | ||||||
|  |             $this->actor = ActivityPub_explorer::get_profile_from_url($this->activity['actor']); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Handle the Activity | ||||||
|  |         $this->process(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Validates if a given Activity is valid. Throws exception if not. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     private function validate_activity() | ||||||
|  |     { | ||||||
|  |         // Activity validation | ||||||
|  |         // Validate data | ||||||
|  |         if (!(isset($this->activity['type']))) { | ||||||
|  |             throw new Exception('Activity Validation Failed: Type was not specified.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($this->activity['actor'])) { | ||||||
|  |             throw new Exception('Activity Validation Failed: Actor was not specified.'); | ||||||
|  |         } | ||||||
|  |         if (!isset($this->activity['object'])) { | ||||||
|  |             throw new Exception('Activity Validation Failed: Object was not specified.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Object validation | ||||||
|  |         switch ($this->activity['type']) { | ||||||
|  |             case 'Accept': | ||||||
|  |                 Activitypub_accept::validate_object($this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Create': | ||||||
|  |                 Activitypub_create::validate_object($this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Delete': | ||||||
|  |             case 'Follow': | ||||||
|  |             case 'Like': | ||||||
|  |             case 'Announce': | ||||||
|  |                 if (!filter_var($this->object, FILTER_VALIDATE_URL)) { | ||||||
|  |                     throw new Exception('Object is not a valid Object URI for Activity.'); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'Undo': | ||||||
|  |                 Activitypub_undo::validate_object($this->object); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new Exception('Unknown Activity Type.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sends the Activity to proper handler in order to be processed. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      */ | ||||||
|  |     private function process() | ||||||
|  |     { | ||||||
|  |         switch ($this->activity['type']) { | ||||||
|  |             case 'Accept': | ||||||
|  |                 $this->handle_accept($this->actor, $this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Create': | ||||||
|  |                 $this->handle_create($this->actor, $this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Delete': | ||||||
|  |                 $this->handle_delete($this->actor, $this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Follow': | ||||||
|  |                 $this->handle_follow($this->actor, $this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Like': | ||||||
|  |                 $this->handle_like($this->actor, $this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Undo': | ||||||
|  |                 $this->handle_undo($this->actor, $this->object); | ||||||
|  |                 break; | ||||||
|  |             case 'Announce': | ||||||
|  |                 $this->handle_announce($this->actor, $this->object); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles an Accept Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_accept($actor, $object) | ||||||
|  |     { | ||||||
|  |         switch ($object['type']) { | ||||||
|  |             case 'Follow': | ||||||
|  |                 $this->handle_accept_follow($actor, $object); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles an Accept Follow Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_accept_follow($actor, $object) | ||||||
|  |     { | ||||||
|  |         // Get valid Object profile | ||||||
|  |         $object_profile = new Activitypub_explorer; | ||||||
|  |         $object_profile = $object_profile->lookup($object['object'])[0]; | ||||||
|  |  | ||||||
|  |         $pending_list = new Activitypub_pending_follow_requests($actor->getID(), $object_profile->getID()); | ||||||
|  |         $pending_list->remove(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Create Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_create($actor, $object) | ||||||
|  |     { | ||||||
|  |         switch ($object['type']) { | ||||||
|  |             case 'Note': | ||||||
|  |                 Activitypub_notice::create_notice($object, $actor); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Delete Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_delete($actor, $object) | ||||||
|  |     { | ||||||
|  |         $notice = ActivityPubPlugin::grab_notice_from_url($object['object']); | ||||||
|  |         $notice->deleteAs($actor); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Follow Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_follow($actor, $object) | ||||||
|  |     { | ||||||
|  |         Activitypub_follow::follow($actor, $object); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Like Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_like($actor, $object) | ||||||
|  |     { | ||||||
|  |         $notice = ActivityPubPlugin::grab_notice_from_url($object); | ||||||
|  |         Fave::addNew($actor, $notice); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Undo Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_undo($actor, $object) | ||||||
|  |     { | ||||||
|  |         switch ($object['type']) { | ||||||
|  |             case 'Follow': | ||||||
|  |                 $this->handle_undo_follow($actor, $object['object']); | ||||||
|  |                 break; | ||||||
|  |             case 'Like': | ||||||
|  |                 $this->handle_undo_like($actor, $object['object']); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Undo Like Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_undo_like($actor, $object) | ||||||
|  |     { | ||||||
|  |         $notice = ActivityPubPlugin::grab_notice_from_url($object); | ||||||
|  |         Fave::removeEntry($actor, $notice); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Undo Follow Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_undo_follow($actor, $object) | ||||||
|  |     { | ||||||
|  |         // Get Object profile | ||||||
|  |         $object_profile = new Activitypub_explorer; | ||||||
|  |         $object_profile = $object_profile->lookup($object)[0]; | ||||||
|  |  | ||||||
|  |         if (Subscription::exists($actor, $object_profile)) { | ||||||
|  |             Subscription::cancel($actor, $object_profile); | ||||||
|  |         // You are no longer following this person. | ||||||
|  |         } else { | ||||||
|  |             // 409: You are not following this person already. | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Announce Activity received by our inbox. | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.upt.pt> | ||||||
|  |      * @param Profile $actor Actor | ||||||
|  |      * @param Array $object Activity | ||||||
|  |      */ | ||||||
|  |     private function handle_announce($actor, $object) | ||||||
|  |     { | ||||||
|  |         $object_notice = ActivityPubPlugin::grab_notice_from_url($object); | ||||||
|  |         $object_notice->repeat($actor, 'ActivityPub'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										320
									
								
								utils/postman.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										320
									
								
								utils/postman.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,15 +20,18 @@ | |||||||
|  * @category  Plugin |  * @category  Plugin | ||||||
|  * @package   GNUsocial |  * @package   GNUsocial | ||||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> |  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> |  | ||||||
|  * @copyright 2018 Free Software Foundation http://fsf.org |  * @copyright 2018 Free Software Foundation http://fsf.org | ||||||
|  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 |  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 | ||||||
|  * @link      https://www.gnu.org/software/social/ |  * @link      https://www.gnu.org/software/social/ | ||||||
|  */ |  */ | ||||||
| if (!defined ('GNUSOCIAL')) { | if (!defined('GNUSOCIAL')) { | ||||||
|         exit (1); |     exit(1); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | use GuzzleHttp\Client; | ||||||
|  | use HttpSignatures\Context; | ||||||
|  | use HttpSignatures\GuzzleHttpSignatures; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * ActivityPub's own Postman |  * ActivityPub's own Postman | ||||||
|  * |  * | ||||||
| @@ -43,54 +46,277 @@ if (!defined ('GNUSOCIAL')) { | |||||||
|  */ |  */ | ||||||
| class Activitypub_postman | class Activitypub_postman | ||||||
| { | { | ||||||
|         private $actor; |     private $actor; | ||||||
|         private $to = array (); |     private $actor_uri; | ||||||
|         private $client; |     private $to = []; | ||||||
|         private $headers; |     private $client; | ||||||
|  |     private $headers; | ||||||
|  |  | ||||||
|         /** |     /** | ||||||
|          * Create a postman to deliver something to someone |      * Create a postman to deliver something to someone | ||||||
|          * |      * | ||||||
|          * @param Activitypub_profile $to array of destinataries |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|          */ |      * @param Profile $from Profile of sender | ||||||
|         public function __construct ($from, $to) |      * @param Array of Activitypub_profile $to destinataries | ||||||
|         { |      */ | ||||||
|                 $this->actor = $from; |     public function __construct($from, $to) | ||||||
|                 $this->to = $to; |     { | ||||||
|                 $this->headers = array(); |         $this->actor = $from; | ||||||
|                 $this->headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; |         $discovery = new Activitypub_explorer(); | ||||||
|                 $this->headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; |         $this->to = $to; | ||||||
|  |         $followers = apActorFollowersAction::generate_followers($this->actor, 0, null); | ||||||
|  |         foreach ($followers as $sub) { | ||||||
|  |             try { | ||||||
|  |                 $to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]); | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 // Not an ActivityPub Remote Follower, let it go | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         unset($discovery); | ||||||
|  |  | ||||||
|  |         $this->actor_uri = ActivityPubPlugin::actor_uri($this->actor); | ||||||
|  |  | ||||||
|  |         $actor_private_key = new Activitypub_rsa(); | ||||||
|  |         $actor_private_key = $actor_private_key->get_private_key($this->actor); | ||||||
|  |  | ||||||
|  |         $context = new Context([ | ||||||
|  |             'keys' => [$this->actor_uri.'#public-key' => $actor_private_key], | ||||||
|  |             'algorithm' => 'rsa-sha256', | ||||||
|  |             'headers' => ['(request-target)', 'date', 'content-type', 'accept', 'user-agent'], | ||||||
|  |         ]); | ||||||
|  |  | ||||||
|  |         $this->headers = [ | ||||||
|  |             'content-type' => 'application/activity+json', | ||||||
|  |             'accept'       => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | ||||||
|  |             'user-agent'   => 'GNUSocialBot v0.1 - https://gnu.io/social', | ||||||
|  |             'date'         => gmdate('D, d M Y H:i:s \G\M\T', time()) | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         $handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context); | ||||||
|  |         $this->client = new Client(['handler' => $handlerStack]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send something to remote instance | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param string $data request body | ||||||
|  |      * @param string $inbox url of remote inbox | ||||||
|  |      * @param string $method request method | ||||||
|  |      * @return Psr\Http\Message\ResponseInterface | ||||||
|  |      */ | ||||||
|  |     public function send($data, $inbox, $method = 'POST') | ||||||
|  |     { | ||||||
|  |         common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox); | ||||||
|  |         $response = $this->client->request($method, $inbox, ['headers' => array_merge($this->headers, ['(request-target)' => strtolower($method).' '.parse_url($inbox, PHP_URL_PATH)]),'body' => $data]); | ||||||
|  |         common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatusCode().': '.$response->getBody()->getContents()); | ||||||
|  |         return $response; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send a follow notification to remote instance | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public function follow() | ||||||
|  |     { | ||||||
|  |         $data = Activitypub_follow::follow_to_array(ActivityPubPlugin::actor_uri($this->actor), $this->to[0]->getUrl()); | ||||||
|  |         $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox()); | ||||||
|  |         $res_body = json_decode($res->getBody()->getContents()); | ||||||
|  |  | ||||||
|  |         if ($res->getStatusCode() == 200 || $res->getStatusCode() == 202 || $res->getStatusCode() == 409) { | ||||||
|  |             $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID()); | ||||||
|  |             $pending_list->add(); | ||||||
|  |             return true; | ||||||
|  |         } elseif (isset($res_body[0]->error)) { | ||||||
|  |             throw new Exception($res_body[0]->error); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         throw new Exception("An unknown error occurred."); | ||||||
|          * Send a follow notification to remote instance |     } | ||||||
|          */ |  | ||||||
|         public function follow () |  | ||||||
|         { |  | ||||||
|                 $this->client = new HTTPClient (); |  | ||||||
|                 $data = array ("@context" => "https://www.w3.org/ns/activitystreams", |  | ||||||
|                           "type"   => "Follow", |  | ||||||
|                           "actor"  => $this->actor->getUrl (), |  | ||||||
|                           "object" => $this->to[0]->getUrl ()); |  | ||||||
|                 $this->client->setBody (json_encode ($data)); |  | ||||||
|                 $response = $this->client->post ($this->to[0]->getInbox (), $this->headers); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |     /** | ||||||
|          * Send a Undo Follow notification to remote instance |      * Send a Undo Follow notification to remote instance | ||||||
|          */ |      * | ||||||
|         public function undo_follow () |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|         { |      */ | ||||||
|                 $this->client = new HTTPClient (); |     public function undo_follow() | ||||||
|                 $data = array ("@context" => "https://www.w3.org/ns/activitystreams", |     { | ||||||
|                             "type"   => "Undo", |         $data = Activitypub_undo::undo_to_array( | ||||||
|                             "actor"  => $this->actor->getUrl (), |                     Activitypub_follow::follow_to_array( | ||||||
|                             "object" => array ( |                         ActivityPubPlugin::actor_uri($this->actor), | ||||||
|                                 "type" => "Follow", |                         $this->to[0]->getUrl() | ||||||
|                                 "object" => $this->to[0]->getUrl () |                     ) | ||||||
|                             ) |                 ); | ||||||
|  |         $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox()); | ||||||
|  |         $res_body = json_decode($res->getBody()->getContents()); | ||||||
|  |  | ||||||
|  |         if ($res->getStatusCode() == 200 || $res->getStatusCode() == 202 || $res->getStatusCode() == 409) { | ||||||
|  |             $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID()); | ||||||
|  |             $pending_list->remove(); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         if (isset($res_body[0]->error)) { | ||||||
|  |             throw new Exception($res_body[0]->error); | ||||||
|  |         } | ||||||
|  |         throw new Exception("An unknown error occurred."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send a Accept Follow notification to remote instance | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      */ | ||||||
|  |     public function accept_follow() | ||||||
|  |     { | ||||||
|  |         $data = Activitypub_accept::accept_to_array( | ||||||
|  |                     Activitypub_follow::follow_to_array( | ||||||
|  |                        $this->to[0]->getUrl(), | ||||||
|  |                        ActivityPubPlugin::actor_uri($this->actor) | ||||||
|  |  | ||||||
|  |                     ) | ||||||
|  |                ); | ||||||
|  |         $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox()); | ||||||
|  |         $res_body = json_decode($res->getBody()->getContents()); | ||||||
|  |  | ||||||
|  |         if ($res->getStatusCode() == 200 || $res->getStatusCode() == 202 || $res->getStatusCode() == 409) { | ||||||
|  |             $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID()); | ||||||
|  |             $pending_list->remove(); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         if (isset($res_body[0]->error)) { | ||||||
|  |             throw new Exception($res_body[0]->error); | ||||||
|  |         } | ||||||
|  |         throw new Exception("An unknown error occurred."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send a Like notification to remote instances holding the notice | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Notice $notice | ||||||
|  |      */ | ||||||
|  |     public function like($notice) | ||||||
|  |     { | ||||||
|  |         $data = Activitypub_like::like_to_array( | ||||||
|  |                     ActivityPubPlugin::actor_uri($this->actor), | ||||||
|  |                     $notice->getUrl() | ||||||
|  |                 ); | ||||||
|  |         $data = json_encode($data, JSON_UNESCAPED_SLASHES); | ||||||
|  |  | ||||||
|  |         foreach ($this->to_inbox() as $inbox) { | ||||||
|  |             $this->send($data, $inbox); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send a Undo Like notification to remote instances holding the notice | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Notice $notice | ||||||
|  |      */ | ||||||
|  |     public function undo_like($notice) | ||||||
|  |     { | ||||||
|  |         $data = Activitypub_undo::undo_to_array( | ||||||
|  |                          Activitypub_like::like_to_array( | ||||||
|  |                              ActivityPubPlugin::actor_uri($this->actor), | ||||||
|  |                             $notice->getUrl() | ||||||
|  |                          ) | ||||||
|  |                 ); | ||||||
|  |         $data = json_encode($data, JSON_UNESCAPED_SLASHES); | ||||||
|  |  | ||||||
|  |         foreach ($this->to_inbox() as $inbox) { | ||||||
|  |             $this->send($data, $inbox); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send a Create notification to remote instances | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Notice $notice | ||||||
|  |      */ | ||||||
|  |     public function create_note($notice) | ||||||
|  |     { | ||||||
|  |         $data = Activitypub_create::create_to_array( | ||||||
|  |                     $this->actor_uri, | ||||||
|  |                     Activitypub_notice::notice_to_array($notice) | ||||||
|  |                 ); | ||||||
|  |         $data = json_encode($data, JSON_UNESCAPED_SLASHES); | ||||||
|  |  | ||||||
|  |         foreach ($this->to_inbox() as $inbox) { | ||||||
|  |             $this->send($data, $inbox); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send a Announce notification to remote instances | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Notice $notice | ||||||
|  |      */ | ||||||
|  |     public function announce($notice) | ||||||
|  |     { | ||||||
|  |         $data = Activitypub_announce::announce_to_array( | ||||||
|  |                          ActivityPubPlugin::actor_uri($this->actor), | ||||||
|  |                          $notice->getUri() | ||||||
|                         ); |                         ); | ||||||
|                 $this->client->setBody (json_encode ($data)); |         $data = json_encode($data, JSON_UNESCAPED_SLASHES); | ||||||
|                 $response = $this->client->post ($this->to[0]->getInbox (), $this->headers); |  | ||||||
|  |         foreach ($this->to_inbox() as $inbox) { | ||||||
|  |             $this->send($data, $inbox); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Send a Delete notification to remote instances holding the notice | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @param Notice $notice | ||||||
|  |      */ | ||||||
|  |     public function delete($notice) | ||||||
|  |     { | ||||||
|  |         $data = Activitypub_delete::delete_to_array( | ||||||
|  |                     ActivityPubPlugin::actor_uri($notice->getProfile()), | ||||||
|  |                     $notice->getUrl() | ||||||
|  |                 ); | ||||||
|  |         $errors = []; | ||||||
|  |         $data = json_encode($data, JSON_UNESCAPED_SLASHES); | ||||||
|  |         foreach ($this->to_inbox() as $inbox) { | ||||||
|  |             $res = $this->send($data, $inbox); | ||||||
|  |             if (!$res->getStatusCode() == 200) { | ||||||
|  |                 $res_body = json_decode($res->getBody()->getContents(), true); | ||||||
|  |                 if (isset($res_body[0]['error'])) { | ||||||
|  |                     $errors[] = ($res_body[0]['error']); | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |                 $errors[] = ("An unknown error occurred."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (!empty($errors)) { | ||||||
|  |             throw new Exception(json_encode($errors)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Clean list of inboxes to deliver messages | ||||||
|  |      * | ||||||
|  |      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||||
|  |      * @return array To Inbox URLs | ||||||
|  |      */ | ||||||
|  |     private function to_inbox() | ||||||
|  |     { | ||||||
|  |         $to_inboxes = []; | ||||||
|  |         foreach ($this->to as $to_profile) { | ||||||
|  |             $i = $to_profile->get_inbox(); | ||||||
|  |             // Prevent delivering to self | ||||||
|  |             if ($i == [common_local_url('apInbox')]) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             $to_inboxes[] = $i; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return array_unique($to_inboxes); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user