Compare commits
	
		
			111 Commits
		
	
	
		
			apfeed
			...
			http-signa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7f704f34fa | ||
|  | 6fbf37b7fe | ||
|  | b2f6f9f08e | ||
|  | 935495415b | ||
|  | 334923e542 | ||
|  | 76bea7b17e | ||
|  | 0475954741 | ||
|  | ca8867c1c0 | ||
|  | fcbdf6aef2 | ||
|  | f3ebbf472b | ||
|  | 287ebeb521 | ||
|  | edc62d4cb0 | ||
|  | b9c76c2901 | ||
|  | 47b1f2219c | ||
|  | c10585da3f | ||
|  | 73d5fa79d7 | ||
|  | 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 | ||||
							
								
								
									
										877
									
								
								ActivityPubPlugin.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										877
									
								
								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 | ||||
| Warning: This is still a work in progress, not every ActivityPub property is fully implemented yet. | ||||
| # ActivityPub plugin for GNU Social 1.0 Alpha | ||||
| 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*. | ||||
|   | ||||
							
								
								
									
										87
									
								
								actions/apactorfollowers.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										87
									
								
								actions/apactorfollowers.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -46,69 +45,91 @@ class apActorFollowersAction extends ManagedAction | ||||
|     /** | ||||
|      * Handle the Followers Collection request | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @return void | ||||
|      */ | ||||
|     protected function handle() | ||||
|     { | ||||
|                 $nickname = $this->trimmed ('nickname'); | ||||
|         try { | ||||
|                         $user    = User::getByNickname ($nickname); | ||||
|                         $profile = $user->getProfile (); | ||||
|                         $url     = $profile->profileurl; | ||||
|             $profile = Profile::getByID($this->trimmed('id')); | ||||
|             $profile_id = $profile->getID(); | ||||
|         } catch (Exception $e) { | ||||
|                         ActivityPubReturn::error ('Invalid username.'); | ||||
|             ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||
|         } | ||||
|  | ||||
|         if (!$profile->isLocal()) { | ||||
|             ActivityPubReturn::error("This is not a local user.", 403); | ||||
|         } | ||||
|  | ||||
|         if (!isset($_GET["page"])) { | ||||
|                         $page = 1; | ||||
|             $page = 0; | ||||
|         } else { | ||||
|             $page = intval($this->trimmed('page')); | ||||
|         } | ||||
|  | ||||
|                 if ($page <= 0) { | ||||
|         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", | ||||
|             'id'           => common_local_url('apActorFollowers', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), | ||||
|             '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 | ||||
|             '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; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										86
									
								
								actions/apactorfollowing.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										86
									
								
								actions/apactorfollowing.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -46,69 +45,90 @@ class apActorFollowingAction extends ManagedAction | ||||
|     /** | ||||
|      * Handle the Following Collection request | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @return void | ||||
|      */ | ||||
|     protected function handle() | ||||
|     { | ||||
|                 $nickname = $this->trimmed ('nickname'); | ||||
|         try { | ||||
|                         $user    = User::getByNickname ($nickname); | ||||
|                         $profile = $user->getProfile (); | ||||
|                         $url     = $profile->profileurl; | ||||
|             $profile = Profile::getByID($this->trimmed('id')); | ||||
|             $profile_id = $profile->getID(); | ||||
|         } catch (Exception $e) { | ||||
|                         ActivityPubReturn::error ('Invalid username.'); | ||||
|             ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||
|         } | ||||
|  | ||||
|         if (!$profile->isLocal()) { | ||||
|             ActivityPubReturn::error("This is not a local user.", 403); | ||||
|         } | ||||
|  | ||||
|         if (!isset($_GET["page"])) { | ||||
|                         $page = 1; | ||||
|             $page = 0; | ||||
|         } else { | ||||
|             $page = intval($this->trimmed('page')); | ||||
|         } | ||||
|  | ||||
|                 if ($page <= 0) { | ||||
|         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", | ||||
|             'id'           => common_local_url('apActorFollowing', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), | ||||
|             '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 | ||||
|             '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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								actions/apactorprofile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										21
									
								
								actions/apactorprofile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -19,7 +19,6 @@ | ||||
|  * | ||||
|  * @category  Plugin | ||||
|  * @package   GNUsocial | ||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> | ||||
|  * @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 | ||||
| @@ -34,7 +33,6 @@ if (!defined ('GNUSOCIAL')) { | ||||
|  * | ||||
|  * @category  Plugin | ||||
|  * @package   GNUsocial | ||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> | ||||
|  * @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/ | ||||
| @@ -47,18 +45,29 @@ class apActorProfileAction extends ManagedAction | ||||
|     /** | ||||
|      * Handle the Actor Profile request | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @return void | ||||
|      */ | ||||
|     protected function handle() | ||||
|     { | ||||
|                 $nickname = $this->trimmed ('nickname'); | ||||
|         if (!empty($id = $this->trimmed('id'))) { | ||||
|             try { | ||||
|                         $user    = User::getByNickname ($nickname); | ||||
|                         $profile = $user->getProfile (); | ||||
|                 $profile = Profile::getByID($id); | ||||
|             } catch (Exception $e) { | ||||
|                 ActivityPubReturn::error('Invalid Actor URI.', 404); | ||||
|             } | ||||
|                 catch (Exception $e) { | ||||
|             unset($id); | ||||
|         } else { | ||||
|             try { | ||||
|                 $profile = User::getByNickname($this->trimmed('nickname'))->getProfile(); | ||||
|             } 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); | ||||
|  | ||||
|   | ||||
| @@ -1,342 +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); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * ActivityPub Feed | ||||
|  * | ||||
|  * @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 apFeedAction extends ManagedAction | ||||
| { | ||||
|         protected $needLogin = false; | ||||
|         protected $canPost = true; | ||||
|  | ||||
|         var $page = 1; | ||||
|         var $count = 20; | ||||
|         var $max_id = 0; | ||||
|         var $since_id = 0; | ||||
|  | ||||
|         /** | ||||
|          * Handle the Actor Inbox request | ||||
|          * | ||||
|          * @return void | ||||
|          */ | ||||
|         protected function handle () | ||||
|         { | ||||
|                 $this->showJsonTimeline($this->getNotices()); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Get notices | ||||
|          * | ||||
|          * @return array notices | ||||
|          */ | ||||
|          function getNotices() | ||||
|          { | ||||
|                 $notices = array (); | ||||
|  | ||||
|                 $stream = new PublicNoticeStream (null); | ||||
|  | ||||
|                 $notice = $stream->getNotices (($this->page - 1) * $this->count, | ||||
|                                               $this->count, | ||||
|                                               $this->since_id, $this->max_id); | ||||
|  | ||||
|                 $notices = $notice->fetchAll (); | ||||
|  | ||||
|                 NoticeList::prefill ($notices); | ||||
|  | ||||
|                 return $notices; | ||||
|         } | ||||
|  | ||||
|         function showJsonTimeline ($notice) | ||||
|         { | ||||
|                 header ('Content-Type: application/json; charset=utf-8'); | ||||
|  | ||||
|                 $statuses = array (); | ||||
|  | ||||
|                 if (is_array ($notice)) { | ||||
|                         //FIXME: make everything calling showJsonTimeline use only Notice objects | ||||
|                         $ids = array (); | ||||
|                         foreach($notice as $n) { | ||||
|                                 $ids[] = $n->getID (); | ||||
|                         } | ||||
|                         $notice = Notice::multiGet ('id', $ids); | ||||
|                 } | ||||
|  | ||||
|                 while ($notice->fetch ()) { | ||||
|                         try { | ||||
|                                 $twitter_status = $this->twitterStatusArray ($notice); | ||||
|                                 array_push ($statuses, $twitter_status); | ||||
|                         } catch (Exception $e) { | ||||
|                                 common_log (LOG_ERR, $e->getMessage ()); | ||||
|                                 continue; | ||||
|                         } | ||||
|                 } | ||||
|  | ||||
|                 $this->showJsonObjects ($statuses); | ||||
|         } | ||||
|  | ||||
|         function showJsonObjects ($objects) | ||||
|         { | ||||
|                 $json_objects = json_encode ($objects); | ||||
|                 if ($json_objects === false) { | ||||
|                         $this-> clientError(_('JSON encoding failed. Error: ').json_last_error_msg ()); | ||||
|                 } else { | ||||
|                         print $json_objects; | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         function twitterStatusArray ($notice, $include_user = true) | ||||
|         { | ||||
|                 $base = $this->twitterSimpleStatusArray ($notice, $include_user); | ||||
|  | ||||
|                 // FIXME: MOVE TO SHARE PLUGIN | ||||
|                 if (!empty ($notice->repeat_of)) { | ||||
|                         $original = Notice::getKV ('id', $notice->repeat_of); | ||||
|                         if ($original instanceof Notice) { | ||||
|                                 $orig_array = $this->twitterSimpleStatusArray($original, $include_user); | ||||
|                                 $base['retweeted_status'] = $orig_array; | ||||
|                         } | ||||
|                 } | ||||
|  | ||||
|                 return $base; | ||||
|         } | ||||
|  | ||||
|         function twitterSimpleStatusArray ($notice, $include_user = true) | ||||
|         { | ||||
|                 $profile = $notice->getProfile (); | ||||
|  | ||||
|                 $twitter_status = array (); | ||||
|                 $twitter_status['text'] = $notice->content; | ||||
|                 $twitter_status['truncated'] = false; | ||||
|                 $twitter_status['created_at'] = self::dateTwitter ($notice->created); | ||||
|                 try { | ||||
|                         // We could just do $notice->reply_to but maybe the future holds a | ||||
|                         // different story for parenting. | ||||
|                         $parent = $notice->getParent (); | ||||
|                         $in_reply_to = $parent->id; | ||||
|                 } catch (NoParentNoticeException $e) { | ||||
|                         $in_reply_to = null; | ||||
|                 } catch (NoResultException $e) { | ||||
|                         // the in_reply_to message has probably been deleted | ||||
|                         $in_reply_to = null; | ||||
|                 } | ||||
|                 $twitter_status['in_reply_to_status_id'] = $in_reply_to; | ||||
|  | ||||
|                 $source = null; | ||||
|                 $source_link = null; | ||||
|  | ||||
|                 $ns = $notice->getSource (); | ||||
|                 if ($ns instanceof Notice_source) { | ||||
|                         $source = $ns->code; | ||||
|                         if (!empty ($ns->url)) { | ||||
|                                 $source_link = $ns->url; | ||||
|                                 if (!empty ($ns->name)) { | ||||
|                                         $source = $ns->name; | ||||
|                                 } | ||||
|                         } | ||||
|                 } | ||||
|  | ||||
|                 $twitter_status['uri'] = $notice->getUri (); | ||||
|                 $twitter_status['source'] = $source; | ||||
|                 $twitter_status['source_link'] = $source_link; | ||||
|                 $twitter_status['id'] = intval ($notice->id); | ||||
|  | ||||
|                 $replier_profile = null; | ||||
|  | ||||
|                 if ($notice->reply_to) { | ||||
|                         $reply = Notice::getKV (intval ($notice->reply_to)); | ||||
|                         if ($reply) { | ||||
|                                 $replier_profile = $reply->getProfile (); | ||||
|                         } | ||||
|                 } | ||||
|  | ||||
|                 $twitter_status['in_reply_to_user_id'] = | ||||
|                     ($replier_profile) ? intval ($replier_profile->id) : null; | ||||
|                 $twitter_status['in_reply_to_screen_name'] = | ||||
|                     ($replier_profile) ? $replier_profile->nickname : null; | ||||
|  | ||||
|                 try { | ||||
|                         $notloc = Notice_location::locFromStored ($notice); | ||||
|                         // This is the format that GeoJSON expects stuff to be in | ||||
|                         $twitter_status['geo'] = array ('type' => 'Point', | ||||
|                                                         'coordinates' => array ( | ||||
|                                                             (float) $notloc->lat, | ||||
|                                                             (float) $notloc->lon)); | ||||
|                 } catch (ServerException $e) { | ||||
|                         $twitter_status['geo'] = null; | ||||
|                 } | ||||
|  | ||||
|                 // Enclosures | ||||
|                 $attachments = $notice->attachments (); | ||||
|  | ||||
|                 if (!empty ($attachments)) { | ||||
|  | ||||
|                         $twitter_status['attachments'] = array (); | ||||
|  | ||||
|                         foreach ($attachments as $attachment) { | ||||
|                                 try { | ||||
|                                         $enclosure_o = $attachment->getEnclosure (); | ||||
|                                         $enclosure = array(); | ||||
|                                         $enclosure['url'] = $enclosure_o->url; | ||||
|                                         $enclosure['mimetype'] = $enclosure_o->mimetype; | ||||
|                                         $enclosure['size'] = $enclosure_o->size; | ||||
|                                         $twitter_status['attachments'][] = $enclosure; | ||||
|                                 } catch (ServerException $e) { | ||||
|                                         // There was not enough metadata available | ||||
|                                 } | ||||
|                         } | ||||
|                 } | ||||
|  | ||||
|                 if ($include_user && $profile) { | ||||
|                         // Don't get notice (recursive!) | ||||
|                         $twitter_user = $this->twitterUserArray($profile, false); | ||||
|                         $twitter_status['user'] = $twitter_user; | ||||
|                 } | ||||
|                 // StatusNet-specific | ||||
|  | ||||
|                 $twitter_status['statusnet_html'] = $notice->getRendered(); | ||||
|                 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation); | ||||
|  | ||||
|                 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array | ||||
|                 Event::handle('NoticeSimpleStatusArray', | ||||
|                               array ($notice, &$twitter_status, $this->scoped, | ||||
|                                     array ('include_user' => $include_user))); | ||||
|  | ||||
|                 return $twitter_status; | ||||
|         } | ||||
|  | ||||
|         static function dateTwitter($dt) | ||||
|         { | ||||
|                 $dateStr = date ('d F Y H:i:s', strtotime ($dt)); | ||||
|                 $d = new DateTime ($dateStr, new DateTimeZone ('UTC')); | ||||
|                 $d->setTimezone (new DateTimeZone(common_timezone())); | ||||
|                 return $d->format ('D M d H:i:s O Y'); | ||||
|         } | ||||
|  | ||||
|         function twitterUserArray ($profile, $get_notice = false) | ||||
|         { | ||||
|                 $twitter_user = array (); | ||||
|  | ||||
|                 try { | ||||
|                         $user = $profile->getUser (); | ||||
|                 } catch (NoSuchUserException $e) { | ||||
|                         $user = null; | ||||
|                 } | ||||
|  | ||||
|                 $twitter_user['id'] = $profile->getID (); | ||||
|                 $twitter_user['name'] = $profile->getBestName (); | ||||
|                 $twitter_user['screen_name'] = $profile->getNickname (); | ||||
|                 $twitter_user['location'] = $profile->location; | ||||
|                 $twitter_user['description'] = $profile->getDescription (); | ||||
|  | ||||
|                 // TODO: avatar url template (example.com/user/avatar?size={x}x{y}) | ||||
|                 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE); | ||||
|                 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url']; | ||||
|  | ||||
|                 // START introduced by qvitter API, not necessary for StatusNet API | ||||
|                 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE); | ||||
|                 try { | ||||
|                         $avatar = Avatar::getUploaded ($profile); | ||||
|                         $origurl = $avatar->displayUrl (); | ||||
|                 } catch(Exception $e) { | ||||
|                         $origurl = $twitter_user['profile_image_url_profile_size']; | ||||
|                 } | ||||
|                 $twitter_user['profile_image_url_original'] = $origurl; | ||||
|  | ||||
|                 $twitter_user['groups_count'] = $profile->getGroupCount(); | ||||
|                 foreach (array('linkcolor', 'backgroundcolor') as $key) { | ||||
|                         $twitter_user[$key] =  Profile_prefs::getConfigData($profile, 'theme', $key); | ||||
|                 } | ||||
|                 // END introduced by qvitter API, not necessary for StatusNet API | ||||
|  | ||||
|                 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null; | ||||
|                 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false; | ||||
|                 $twitter_user['followers_count'] = $profile->subscriberCount(); | ||||
|  | ||||
|                 // Note: some profiles don't have an associated user | ||||
|  | ||||
|                 $twitter_user['friends_count'] = $profile->subscriptionCount (); | ||||
|  | ||||
|                 $twitter_user['created_at'] = self::dateTwitter($profile->created); | ||||
|  | ||||
|                 $timezone = 'UTC'; | ||||
|  | ||||
|                 if (!empty($user) && $user->timezone) { | ||||
|                         $timezone = $user->timezone; | ||||
|                 } | ||||
|  | ||||
|                 $t = new DateTime; | ||||
|                 $t->setTimezone (new DateTimeZone ($timezone)); | ||||
|  | ||||
|                 $twitter_user['utc_offset']     = $t->format('Z'); | ||||
|                 $twitter_user['time_zone']      = $timezone; | ||||
|                 $twitter_user['statuses_count'] = $profile->noticeCount(); | ||||
|  | ||||
|                 // Is the requesting user following this user? | ||||
|                 // These values might actually also mean "unknown". Ambiguity issues? | ||||
|                 $twitter_user['following']          = false; | ||||
|                 $twitter_user['statusnet_blocking'] = false; | ||||
|                 $twitter_user['notifications']      = false; | ||||
|  | ||||
|                 if ($this->scoped instanceof Profile) { | ||||
|                         try { | ||||
|                                 $sub = Subscription::getSubscription($this->scoped, $profile); | ||||
|                                 // Notifications on? | ||||
|                                 $twitter_user['following'] = true; | ||||
|                                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms); | ||||
|                         } catch (NoResultException $e) { | ||||
|                                 // well, the values are already false... | ||||
|                         } | ||||
|                         $twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile); | ||||
|                 } | ||||
|  | ||||
|                 if ($get_notice) { | ||||
|                         $notice = $profile->getCurrentNotice (); | ||||
|                         if ($notice instanceof Notice) { | ||||
|                                 // don't get user! | ||||
|                                 $twitter_user['status'] = $this->twitterStatusArray($notice, false); | ||||
|                         } | ||||
|                 } | ||||
|                 // StatusNet-specific | ||||
|  | ||||
|                 $twitter_user['statusnet_profile_url'] = $profile->profileurl; | ||||
|  | ||||
|                 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array | ||||
|                 Event::handle('TwitterUserArray', array ($profile, &$twitter_user, $this->scoped, array())); | ||||
|  | ||||
|                 return $twitter_user; | ||||
|         } | ||||
| } | ||||
							
								
								
									
										136
									
								
								actions/apinbox.php
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										136
									
								
								actions/apinbox.php
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| <?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/ | ||||
|  */ | ||||
|  | ||||
| use GuzzleHttp\Psr7; | ||||
| use HttpSignatures\Context; | ||||
|  | ||||
| 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)); | ||||
|         try { | ||||
|             $res = HTTPSignature::parse($headers); | ||||
|             common_debug('ActivityPub Inbox: Request Res: '.print_r($res, true)); | ||||
|         } catch (HttpSignatureError $e) { | ||||
|             common_debug('ActivityPub Inbox: HTTP Signature Error: '. $e->getMessage()); | ||||
|             ActivityPubReturn::error('HTTP Signature Error: '. $e->getMessage()); | ||||
|         } catch (Exception $e) { | ||||
|             ActivityPubReturn::error($e->getMessage()); | ||||
|         } | ||||
|  | ||||
|         /*if (HTTPSignature::verify($res, | ||||
|                 $actor_public_key, 'rsa') == FALSE) { | ||||
|            common_debug('ActivityPub Inbox: Could not authorize request.'); | ||||
|            ActivityPubReturn::error('Unauthorized.', 403); | ||||
|         }*/ | ||||
|  | ||||
|         $context = new Context([ | ||||
|             'keys' => [$res['params']['keyId'] => $actor_public_key], | ||||
|             'algorithm' => $res['params']['algorithm'], | ||||
|             'headers' => $res['headers'], | ||||
|         ]); | ||||
|  | ||||
|         $request = new Psr7\Request($_SERVER['REQUEST_METHOD'], | ||||
|                 (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]", | ||||
|                 $headers); | ||||
|  | ||||
|         if ($context->verifier()->isValid($request) == false) | ||||
|         { | ||||
|             common_debug('ActivityPub Inbox: HTTP Signature: Unauthorized request.'); | ||||
|             ActivityPubReturn::error('Unauthorized.', 403); | ||||
|         } | ||||
|  | ||||
|         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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										49
									
								
								actions/inbox/Follow.php → actions/apnotice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										49
									
								
								actions/inbox/Follow.php → actions/apnotice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -29,30 +28,40 @@ if (!defined ('GNUSOCIAL')) { | ||||
|     exit(1); | ||||
| } | ||||
| 
 | ||||
| // Validate Object
 | ||||
| if (!is_string ($data->object)) { | ||||
|         ActivityPubReturn::error ("Invalid Object object, URL expected."); | ||||
| } | ||||
| /** | ||||
|  * Notice (Local notices only) | ||||
|  * | ||||
|  * @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
 | ||||
|     /** | ||||
|      * Handle the Notice request | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @return void | ||||
|      */ | ||||
|     protected function handle() | ||||
|     { | ||||
|         try { | ||||
|         $object_profile = new Activitypub_explorer; | ||||
|         $object_profile = $object_profile->lookup ($data->object)[0]; | ||||
|             $notice = Notice::getByID($this->trimmed('id')); | ||||
|         } catch (Exception $e) { | ||||
|         ActivityPubReturn::error ("Invalid Object Actor URL.", 404); | ||||
|             ActivityPubReturn::error('Invalid Notice URI.', 404); | ||||
|         } | ||||
| 
 | ||||
| try { | ||||
|         if (!Subscription::exists ($actor_profile, $object_profile)) { | ||||
|                 Subscription::start ($actor_profile, $object_profile); | ||||
|                 $res = array ("@context" => "https://www.w3.org/ns/activitystreams", | ||||
|                           "type"   => "Follow", | ||||
|                           "actor"  => $data->actor, | ||||
|                           "object" => $data->object); | ||||
|         if (!$notice->isLocal()) { | ||||
|             ActivityPubReturn::error("This is not a local notice.", 403); | ||||
|         } | ||||
| 
 | ||||
|         $res = Activitypub_notice::notice_to_array($notice); | ||||
| 
 | ||||
|         ActivityPubReturn::answer($res); | ||||
|         } else { | ||||
|                 ActivityPubReturn::error ("Already following.", 409); | ||||
|     } | ||||
| } catch (Exception $e) { | ||||
|         ActivityPubReturn::error ("Invalid Object Actor URL.", 404); | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										38
									
								
								actions/inbox/Like.php → classes/Activitypub_announce.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										38
									
								
								actions/inbox/Like.php → classes/Activitypub_announce.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -29,13 +28,32 @@ if (!defined ('GNUSOCIAL')) { | ||||
|     exit(1); | ||||
| } | ||||
| 
 | ||||
| try { | ||||
|         Fave::addNew ($actor_profile, Notice::getByUri ($data->object)); | ||||
|         $res = array ("@context" => "https://www.w3.org/ns/activitystreams", | ||||
|                   "type"   => "Like", | ||||
|                   "actor"  => $data->actor, | ||||
|                   "object" => $data->object); | ||||
|         ActivityPubReturn::answer ($res); | ||||
| } catch (Exception $e) { | ||||
|         ActivityPubReturn::error ($e->getMessage (), 403); | ||||
| /** | ||||
|  * 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_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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								classes/Activitypub_attachment.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										19
									
								
								classes/Activitypub_attachment.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -43,29 +42,23 @@ class Activitypub_attachment extends Managed_DataObject | ||||
|     /** | ||||
|      * Generates a pretty array from an Attachment object | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param Attachment $attachment | ||||
|      * @return pretty array to be used in a response | ||||
|      */ | ||||
|     public static function attachment_to_array($attachment) | ||||
|     { | ||||
|         $res = [ | ||||
|                         '@context'  => [ | ||||
|                                 "https://www.w3.org/ns/activitystreams", | ||||
|                                 [ | ||||
|                                         "@language" => "en" | ||||
|                                 ] | ||||
|                         ], | ||||
|                         'id'       => $attachment->getID (), | ||||
|                         'mimetype' => $attachment->mimetype, | ||||
|             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||
|             'type'      => 'Document', | ||||
|             'mediaType' => $attachment->mimetype, | ||||
|             'url'       => $attachment->getUrl(), | ||||
|             'size'      => intval($attachment->size), // $attachment->getSize () | ||||
|                         'title'    => $attachment->getTitle (), | ||||
|                         'meta'     => null | ||||
|             'name'      => $attachment->getTitle(), | ||||
|         ]; | ||||
|  | ||||
|         // Image | ||||
|                 if (substr ($res["mimetype"], 0, 5) == "image") | ||||
|                 { | ||||
|         if (substr($res["mediaType"], 0, 5) == "image") { | ||||
|             $res["meta"]= [ | ||||
|                 'width'  => $attachment->width, | ||||
|                 'height' => $attachment->height | ||||
|   | ||||
							
								
								
									
										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/Delete.php → classes/Activitypub_delete.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										39
									
								
								actions/inbox/Delete.php → classes/Activitypub_delete.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -29,13 +28,33 @@ if (!defined ('GNUSOCIAL')) { | ||||
|     exit(1); | ||||
| } | ||||
| 
 | ||||
| try { | ||||
|         Activitypub_notice::getByUri ($data->object)->deleteAs ($actor_profile); | ||||
|         $res = array ("@context" => "https://www.w3.org/ns/activitystreams", | ||||
|                   "type"   => "Delete", | ||||
|                   "actor"  => $data->actor, | ||||
|                   "object" => $data->object); | ||||
|         ActivityPubReturn::answer ($res); | ||||
| } catch (Exception $e) { | ||||
|         ActivityPubReturn::error ($e->getMessage (), 403); | ||||
| /** | ||||
|  * 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_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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								classes/Activitypub_error.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										2
									
								
								classes/Activitypub_error.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -43,6 +42,7 @@ class Activitypub_error extends Managed_DataObject | ||||
|     /** | ||||
|      * Generates a pretty error from a string | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param string $m | ||||
|      * @return pretty array to be used in a response | ||||
|      */ | ||||
|   | ||||
							
								
								
									
										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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										240
									
								
								classes/Activitypub_notice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										240
									
								
								classes/Activitypub_notice.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -19,7 +19,6 @@ | ||||
|  * | ||||
|  * @category  Plugin | ||||
|  * @package   GNUsocial | ||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> | ||||
|  * @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 | ||||
| @@ -34,7 +33,6 @@ if (!defined ('GNUSOCIAL')) { | ||||
|  * | ||||
|  * @category  Plugin | ||||
|  * @package   GNUsocial | ||||
|  * @author    Daniel Supernault <danielsupernault@gmail.com> | ||||
|  * @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/ | ||||
| @@ -44,46 +42,250 @@ class Activitypub_notice extends Managed_DataObject | ||||
|     /** | ||||
|      * Generates a pretty notice from a Notice object | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param Notice $notice | ||||
|      * @return pretty array to be used in a response | ||||
|      */ | ||||
|     public static function notice_to_array($notice) | ||||
|     { | ||||
|                 $attachments = array (); | ||||
|         $profile = $notice->getProfile(); | ||||
|         $attachments = []; | ||||
|         foreach ($notice->attachments() as $attachment) { | ||||
|             $attachments[] = Activitypub_attachment::attachment_to_array($attachment); | ||||
|         } | ||||
|  | ||||
|                 $tags = array (); | ||||
|         $tags = []; | ||||
|         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"); | ||||
|         $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 = [ | ||||
|                         'id'           => $notice->getUrl (), | ||||
|                         'type'         => 'Notice', | ||||
|                         'actor'        => $notice->getProfile ()->getUrl (), | ||||
|                         'published'    => $notice->getCreated (), | ||||
|                         'to'           => $to, | ||||
|                         'content'      => $notice->getContent (), | ||||
|             '@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(), | ||||
|                         'reply_to'     => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUrl (), | ||||
|                         'is_local'     => $notice->isLocal (), | ||||
|                         'conversation' => intval ($notice->conversation), | ||||
|             '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(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										383
									
								
								classes/Activitypub_profile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										383
									
								
								classes/Activitypub_profile.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -35,103 +34,126 @@ if (!defined ('GNUSOCIAL')) { | ||||
|  * @category  Plugin | ||||
|  * @package   GNUsocial | ||||
|  * @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 | ||||
|  * @link      http://www.gnu.org/software/social/ | ||||
|  */ | ||||
| class Activitypub_profile extends Profile | ||||
| class Activitypub_profile extends Managed_DataObject | ||||
| { | ||||
|     public $__table = 'Activitypub_profile'; | ||||
|  | ||||
|         protected $_profile = null; | ||||
|     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 | ||||
|  | ||||
|     /** | ||||
|      * Return table definition for Schema setup and DB_DataObject usage. | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @return array array of column definitions | ||||
|      */ | ||||
|         static function schemaDef () | ||||
|     public static function schemaDef() | ||||
|     { | ||||
|             return array ( | ||||
|                 'fields' => array ( | ||||
|                     'uri' => array ('type' => 'varchar', 'length' => 191, 'not null' => true), | ||||
|                     'profile_id' => array ('type' => 'integer'), | ||||
|                     'inboxuri' => array ('type' => 'varchar', 'length' => 191), | ||||
|                     'sharedInboxuri' => array ('type' => 'varchar', 'length' => 191), | ||||
|                     'created' => array ('type' => 'datetime', 'not null' => true), | ||||
|                     'modified' => array ('type' => 'datetime', 'not null' => true), | ||||
|                 ), | ||||
|                 'primary key' => array ('uri'), | ||||
|                 'unique keys' => array ( | ||||
|                     'Activitypub_profile_profile_id_key' => array ('profile_id'), | ||||
|                     'Activitypub_profile_inboxuri_key' => array ('inboxuri'), | ||||
|                 ), | ||||
|                 'foreign keys' => array ( | ||||
|                     'Activitypub_profile_profile_id_fkey' => array ('profile', array ('profile_id' => 'id')), | ||||
|                 ), | ||||
|             ); | ||||
|         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']], | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generates a pretty profile from a Profile object | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param Profile $profile | ||||
|      * @return pretty array to be used in a response | ||||
|      */ | ||||
|     public static function profile_to_array($profile) | ||||
|     { | ||||
|                 $url = $profile->getURL (); | ||||
|         $uri = ActivityPubPlugin::actor_uri($profile); | ||||
|         $id = $profile->getID(); | ||||
|         $rsa = new Activitypub_rsa(); | ||||
|         $public_key = $rsa->ensure_public_key($profile); | ||||
|         unset($rsa); | ||||
|         $res = [ | ||||
|                         '@context'        => [ | ||||
|                                 "https://www.w3.org/ns/activitystreams", | ||||
|                                 [ | ||||
|                                         "@language"   => "en" | ||||
|                                 ] | ||||
|                         ], | ||||
|                         'id'              => $profile->getID (), | ||||
|             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||
|             'id'                => $uri, | ||||
|             'type'              => 'Person', | ||||
|                         'nickname'        => $profile->getNickname (), | ||||
|                         'is_local'        => $profile->isLocal (), | ||||
|                         'inbox'           => "{$url}/inbox.json", | ||||
|                         'sharedInbox'     => common_root_url ()."inbox.json", | ||||
|                         'outbox'          => "{$url}/outbox.json", | ||||
|                         'display_name'    => $profile->getFullname (), | ||||
|                         'followers'       => "{$url}/followers.json", | ||||
|                         'followers_count' => $profile->subscriberCount (), | ||||
|                         'following'       => "{$url}/following.json", | ||||
|                         'following_count' => $profile->subscriptionCount (), | ||||
|                         'liked'           => "{$url}/liked.json", | ||||
|                         'liked_count'     => Fave::countByProfile ($profile), | ||||
|             'following'         => common_local_url('apActorFollowing', ['id' => $id]), | ||||
|             'followers'         => common_local_url('apActorFollowers', ['id' => $id]), | ||||
|             'liked'             => common_local_url('apActorLiked', ['id' => $id]), | ||||
|             'inbox'             => common_local_url('apInbox', ['id' => $id]), | ||||
|             'outbox'            => common_local_url('apActorOutbox', ['id' => $id]), | ||||
|             'preferredUsername' => $profile->getNickname(), | ||||
|             'name'              => $profile->getBestName(), | ||||
|             'summary'           => ($desc = $profile->getDescription()) == null ? "" : $desc, | ||||
|                         'url'             => $profile->getURL (), | ||||
|                         'avatar'          => [ | ||||
|             'url'               => $profile->getUrl(), | ||||
|             'manuallyApprovesFollowers' => false, | ||||
|             'publicKey' => [ | ||||
|                 'id'    => $uri."#public-key", | ||||
|                 'owner' => $uri, | ||||
|                 'publicKeyPem' => $public_key | ||||
|             ], | ||||
|             'tag' => [], | ||||
|             'attachment' => [], | ||||
|             'icon' => [ | ||||
|                 'type'      => 'Image', | ||||
|                                 'width'  => 96, | ||||
|                                 'height' => 96, | ||||
|                 '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; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|          * Insert the current objects variables into the database | ||||
|      * Insert the current object variables into the database | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @access public | ||||
|      * @throws ServerException | ||||
|      */ | ||||
|         public function doInsert () | ||||
|     public function do_insert() | ||||
|     { | ||||
|         $profile = new Profile(); | ||||
|  | ||||
|         $profile->created = $this->created = $this->modified = common_sql_now(); | ||||
|  | ||||
|                 $fields = array ( | ||||
|         $fields = [ | ||||
|                     'uri'      => 'profileurl', | ||||
|                     'nickname' => 'nickname', | ||||
|                     'fullname' => 'fullname', | ||||
|                     'bio'      => 'bio' | ||||
|                             ); | ||||
|                     ]; | ||||
|  | ||||
|         foreach ($fields as $af => $pf) { | ||||
|             $profile->$pf = $this->$af; | ||||
| @@ -147,16 +169,19 @@ class Activitypub_profile extends Profile | ||||
|  | ||||
|         if ($ok === false) { | ||||
|             $profile->query('ROLLBACK'); | ||||
|             $this->query('ROLLBACK'); | ||||
|             throw new ServerException('Cannot save ActivityPub profile.'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetch the locally stored profile for this Activitypub_profile | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @return Profile | ||||
|      * @throws NoProfileException if it was not found | ||||
|      */ | ||||
|         public function localProfile () | ||||
|     public function local_profile() | ||||
|     { | ||||
|         $profile = Profile::getKV('id', $this->profile_id); | ||||
|         if (!$profile instanceof Profile) { | ||||
| @@ -168,32 +193,81 @@ class Activitypub_profile extends 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 | ||||
|      */ | ||||
|         static function fromProfile (Profile $profile) | ||||
|     public static function from_profile(Profile $profile) | ||||
|     { | ||||
|         $profile_id = $profile->getID(); | ||||
|  | ||||
|                 $aprofile = Activitypub_profile::getKV ('profile_id', $profile_id); | ||||
|         $aprofile = self::getKV('profile_id', $profile_id); | ||||
|         if (!$aprofile instanceof Activitypub_profile) { | ||||
|                         throw new Exception('No Activitypub_profile for Profile ID: '.$profile_id); | ||||
|             // 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.'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|                 foreach ($profile as $key => $value) { | ||||
|                         $aprofile->$key = $value; | ||||
|         $fields = [ | ||||
|                     'uri'      => 'profileurl', | ||||
|                     'nickname' => 'nickname', | ||||
|                     'fullname' => 'fullname', | ||||
|                     'bio'      => 'bio' | ||||
|                     ]; | ||||
|  | ||||
|         foreach ($fields as $af => $pf) { | ||||
|             $aprofile->$af = $profile->$pf; | ||||
|         } | ||||
|  | ||||
|         return $aprofile; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 getInbox () | ||||
|     public function get_inbox() | ||||
|     { | ||||
|         if (is_null($this->sharedInboxuri)) { | ||||
|             return $this->inboxuri; | ||||
| @@ -201,4 +275,195 @@ class Activitypub_profile extends Profile | ||||
|  | ||||
|         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; | ||||
|             } 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 | ||||
|         // XXX: try FOAF | ||||
|  | ||||
|         // 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; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										33
									
								
								actions/inbox/Announce.php → classes/Activitypub_reject.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										33
									
								
								actions/inbox/Announce.php → classes/Activitypub_reject.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -29,9 +28,31 @@ if (!defined ('GNUSOCIAL')) { | ||||
|     exit(1); | ||||
| } | ||||
| 
 | ||||
| try { | ||||
|         Notice::getByUri ($data->object)->repeat ($actor_profile, "ActivityPub"); | ||||
|         ActivityPubReturn::answer ("Notice repeated successfully."); | ||||
| } catch (Exception $e) { | ||||
|         ActivityPubReturn::error ($e->getMessage (), 403); | ||||
| /** | ||||
|  * 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_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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										12
									
								
								classes/Activitypub_tag.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										12
									
								
								classes/Activitypub_tag.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -43,22 +42,17 @@ class Activitypub_tag extends Managed_DataObject | ||||
|     /** | ||||
|      * Generates a pretty tag from a Tag object | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param Tag $tag | ||||
|      * @return pretty array to be used in a response | ||||
|      */ | ||||
|     public static function tag_to_array($tag) | ||||
|     { | ||||
|         $res = [ | ||||
|                 '@context'          => [ | ||||
|                 "https://www.w3.org/ns/activitystreams", | ||||
|                 [ | ||||
|                   "@language" => "en" | ||||
|                 ] | ||||
|                 ], | ||||
|             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||
|             'name' => $tag, | ||||
|                 'url'  => common_local_url ('tag', array('tag' => $tag)) | ||||
|             'url'  => common_local_url('tag', ['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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										363
									
								
								utils/explorer.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										363
									
								
								utils/explorer.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -32,7 +31,7 @@ if (!defined ('GNUSOCIAL')) { | ||||
| /** | ||||
|  * 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 | ||||
|  * @package   GNUsocial | ||||
| @@ -42,19 +41,44 @@ if (!defined ('GNUSOCIAL')) { | ||||
|  */ | ||||
| class Activitypub_explorer | ||||
| { | ||||
|         private $discovered_actor_profiles = array (); | ||||
|     private $discovered_actor_profiles = []; | ||||
|  | ||||
|     /** | ||||
|      * Shortcut function to get a single profile from its URL. | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param string $url | ||||
|      * @return Profile | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public static function get_profile_from_url($url) | ||||
|     { | ||||
|         $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.'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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) | ||||
|     { | ||||
|                 $this->discovered_actor_profiles = array (); | ||||
|         if (in_array($url, ACTIVITYPUB_PUBLIC_TO)) { | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         common_debug('ActivityPub Explorer: Started now looking for '.$url); | ||||
|         $this->discovered_actor_profiles = []; | ||||
|  | ||||
|         return $this->_lookup($url); | ||||
|     } | ||||
| @@ -64,6 +88,7 @@ class Activitypub_explorer | ||||
|      * 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 | ||||
|      */ | ||||
| @@ -72,43 +97,96 @@ class Activitypub_explorer | ||||
|         // 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"); | ||||
|             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 | ||||
|      * This ensures that we are using a valid ActivityPub URI | ||||
|      * | ||||
|          * @param string $url User's url | ||||
|          * @return boolean success state | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param string $url | ||||
|      * @return boolean success state (related to the response) | ||||
|      * @throws Exception (If the HTTP request fails) | ||||
|      */ | ||||
|         private function grab_local_user ($url) | ||||
|     private function ensure_proper_remote_uri($url) | ||||
|     { | ||||
|                 if (($actor_profile = self::get_profile_by_url ($url)) != false) { | ||||
|                         $this->discovered_actor_profiles[]= $actor_profile; | ||||
|         $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 { | ||||
|                         /******************************** 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)                        * | ||||
|                          **********************************************************************/ | ||||
|             common_debug('ActivityPub Explorer: Invalid potential remote actor while ensuring URI: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES)); | ||||
|         } | ||||
|  | ||||
|                         // Iff we really are in the same instance | ||||
|                         $root_url_len = strlen (common_root_url ()); | ||||
|                         if (substr ($url, 0, $root_url_len) == common_root_url ()) { | ||||
|                                 // Grab the nickname and try to get the user | ||||
|                                 if (($actor_profile = Profile::getKV ("nickname", substr ($url, $root_url_len))) != false) { | ||||
|                                         $this->discovered_actor_profiles[]= $actor_profile; | ||||
|         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; | ||||
|     } | ||||
|  | ||||
| @@ -116,70 +194,157 @@ class Activitypub_explorer | ||||
|      * 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   = array(); | ||||
|             $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 (), 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"]); | ||||
|             $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; | ||||
|                 } else if ($this->validate_remote_response ($res)) { | ||||
|         } 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["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->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->doInsert (); | ||||
|         $aprofile->do_insert(); | ||||
|         $profile = $aprofile->local_profile(); | ||||
|  | ||||
|                 return $aprofile->localProfile (); | ||||
|         // 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 | ||||
|      */ | ||||
|         private function validate_remote_response ($res) | ||||
|     public static function validate_remote_response($res) | ||||
|     { | ||||
|                 if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"], $res["inbox"], $res["sharedInbox"])) { | ||||
|         if (!isset($res['id'], $res['preferredUsername'], $res['inbox'], $res['publicKey']['publicKeyPem'])) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
| @@ -187,20 +352,21 @@ class Activitypub_explorer | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|          * Get a profile from it's profileurl | ||||
|      * 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|Profile false if fails | Profile object if successful | ||||
|      * @return boolean|Activitypub_profile false if fails | Aprofile object if successful | ||||
|      */ | ||||
|         static function get_profile_by_url ($v) | ||||
|     public static function get_aprofile_by_url($v) | ||||
|     { | ||||
|                 $i = Managed_DataObject::getcached(Profile, "profileurl", $v); | ||||
|         $i = Managed_DataObject::getcached("Activitypub_profile", "uri", $v); | ||||
|         if (empty($i)) { // false = cache miss | ||||
|                         $i = new Profile; | ||||
|                         $result = $i->get ("profileurl", $v); | ||||
|             $i = new Activitypub_profile; | ||||
|             $result = $i->get("uri", $v); | ||||
|             if ($result) { | ||||
|                 // Hit! | ||||
|                 $i->encache(); | ||||
| @@ -210,4 +376,89 @@ class Activitypub_explorer | ||||
|         } | ||||
|         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; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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) { | ||||
|             if ($this->_lookup($profile) == false) { | ||||
|                 common_debug('ActivityPub Explorer: Found an invalid actor for '.$profile); | ||||
|                 // TODO: Invalid actor found, fallback to OStatus | ||||
|             } | ||||
|         } | ||||
|         // Go through entire collection | ||||
|         if (!is_null($res["next"])) { | ||||
|             $this->_lookup($res["next"]); | ||||
|         } | ||||
|  | ||||
|         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) | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @param string $url User's url | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public static function get_remote_user_activity($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 (Activitypub_explorer::validate_remote_response($res)) { | ||||
|             common_debug('ActivityPub Explorer: Found a valid remote actor for '.$url); | ||||
|             return $res; | ||||
|         } | ||||
|         throw new Exception('ActivityPub Explorer: Failed to get activity.'); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										348
									
								
								utils/http-signature-auth.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										348
									
								
								utils/http-signature-auth.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,348 @@ | ||||
| <?php | ||||
| /* | ||||
|  * Copyright (c) 2014 David Gwynne <david@gwynne.id.au> | ||||
|  * | ||||
|  * Permission to use, copy, modify, and distribute this software for any | ||||
|  * purpose with or without fee is hereby granted, provided that the above | ||||
|  * copyright notice and this permission notice appear in all copies. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||||
|  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||||
|  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||||
|  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||||
|  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||||
|  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | ||||
|  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| class HttpSignatureError extends Exception { }; | ||||
| class ExpiredRequestError extends HttpSignatureError { }; | ||||
| class InvalidHeaderError extends HttpSignatureError { }; | ||||
| class InvalidParamsError extends HttpSignatureError { }; | ||||
| class MissingHeaderError extends HttpSignatureError { }; | ||||
| class InvalidAlgorithmError extends HttpSignatureError { }; | ||||
|  | ||||
| class HTTPSignature { | ||||
|  | ||||
| 	static function parse(array $headers, array $options = array()) | ||||
| 	{ | ||||
| 		if (!array_key_exists('signature', $headers)) { | ||||
| 			throw new MissingHeaderError('no signature header in the request'); | ||||
| 		} | ||||
| 		$auth = 'Signature '.$headers['signature']; | ||||
|  | ||||
| 		if (!array_key_exists('headers', $options)) { | ||||
| 			$options['headers'] = array(isset($headers['x-date']) ? 'x-date' : 'date'); | ||||
| 		} else { | ||||
| 			if (!is_array($options['headers'])) { | ||||
| 				throw new Exception('headers option is not an array'); | ||||
| 			} | ||||
| 			if (sizeof(array_filter($options['headers'], function ($a) { return (!is_string($a)); }))) { | ||||
| 				throw new Exception('headers option is not an array of strings'); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (!array_key_exists('clockSkew', $options)) { | ||||
| 			$options['clockSkew'] = 300; | ||||
| 		} elseif (!is_numeric($options['clockSkew'])) { | ||||
| 			throw new Exception('clockSkew option is not numeric'); | ||||
| 		} | ||||
|  | ||||
| 		if (array_key_exists('algorithms', $options)) { | ||||
| 			if (!is_array($options['algorithms'])) { | ||||
| 				throw new Exception('algorithms option is not an array'); | ||||
| 			} | ||||
| 			if (sizeof(array_filter($options['algorithms'], function ($a) { return (!is_string($a)); }))) { | ||||
| 				throw new Exception('algorithms option is not an array of strings'); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		$headers['request-line'] = array_key_exists('requestLine', $options) ? | ||||
| 		    $options['requestLine'] : | ||||
| 		    sprintf("%s %s %s", $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']); | ||||
|  | ||||
| 		foreach ($options['headers'] as $header) { | ||||
| 			if (!array_key_exists($header, $headers)) { | ||||
| 				throw new MissingHeaderError("$header was not in the request"); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		$states = array( | ||||
| 			'start' => 0, | ||||
| 			'scheme' => 1, | ||||
| 			'space' => 2, | ||||
| 			'param' => 3, | ||||
| 			'quote' => 4, | ||||
| 			'value' => 5, | ||||
| 			'comma' => 6 | ||||
| 		); | ||||
|  | ||||
| 		$scheme = ''; | ||||
| 		$params = array(); | ||||
|  | ||||
| 		$param = ''; | ||||
| 		$value = ''; | ||||
| 		$state = $states['start']; | ||||
|  | ||||
| 		for ($i = 0; $i < strlen($auth); $i++) { | ||||
| 			$ch = $auth[$i]; | ||||
|  | ||||
| 			switch ($state) { | ||||
| 			case $states['start']: | ||||
| 				if (ctype_space($ch)) { | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				$state = $states['scheme']; | ||||
| 				/* FALLTHROUGH */ | ||||
| 			case $states['scheme']: | ||||
| 				if (ctype_space($ch)) { | ||||
| 					$state = $states['space']; | ||||
| 				} else { | ||||
| 					$scheme .= $ch; | ||||
| 				} | ||||
|  | ||||
| 				break; | ||||
|  | ||||
| 			case $states['space']; | ||||
| 				if (ctype_space($ch)) { | ||||
| 					continue; | ||||
| 				} | ||||
|  | ||||
| 				$state = $states['param']; | ||||
| 				/* FALLTHROUGH */ | ||||
| 			case $states['param']: | ||||
| 				if ($ch === '=') { | ||||
| 					if ($param === '') { | ||||
| 						throw new InvalidHeaderError('bad param name'); | ||||
| 					} | ||||
| 					if (array_key_exists($param, $params)) { | ||||
| 						throw new InvalidHeaderError('param specified again'); | ||||
| 					} | ||||
|  | ||||
| 					$state = $states['quote']; | ||||
| 					break; | ||||
| 				} | ||||
| 				if (!ctype_alpha($ch)) { | ||||
| 					throw new InvalidHeaderError('bad param format'); | ||||
| 				} | ||||
|  | ||||
| 				$param .= $ch; | ||||
| 				break; | ||||
|  | ||||
| 			case $states['quote']; | ||||
| 				if ($ch !== '"') { | ||||
| 					throw new InvalidHeaderError('bad param format'); | ||||
| 				} | ||||
| 				$state = $states['value']; | ||||
| 				break; | ||||
|  | ||||
| 			case $states['value']: | ||||
| 				if ($ch === '"') { | ||||
| 					$params[$param] = $value; | ||||
| 					$param = ''; | ||||
| 					$value = ''; | ||||
|  | ||||
| 					$state = $states['comma']; | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				$value .= $ch; | ||||
| 				break; | ||||
|  | ||||
| 			case $states['comma']: | ||||
| 				if ($ch !== ',') { | ||||
| 					throw new InvalidHeaderError('bad param format'); | ||||
| 				} | ||||
|  | ||||
| 				$state = $states['param']; | ||||
| 				break; | ||||
|  | ||||
| 			default: | ||||
| 				throw new Error('invalid state'); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if ($state !== $states['comma']) { | ||||
| 			throw new InvalidHeaderError("bad param format"); | ||||
| 		} | ||||
|  | ||||
| 		if ($scheme !== 'Signature') { | ||||
| 			throw new InvalidHeaderError('scheme was not "Signature"'); | ||||
| 		} | ||||
| 		$required = array('keyId', 'algorithm', 'signature'); | ||||
| 		foreach ($required as $param) { | ||||
| 			if (!array_key_exists($param, $params)) { | ||||
| 				throw new InvalidHeaderError("$param was not specified"); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (array_key_exists('headers', $params)) { | ||||
| 			$params['headers'] = explode(' ', $params['headers']); | ||||
| 		} else { | ||||
| 			$params['headers'] = array(isset($headers['x-date']) ? 'x-date' : 'date'); | ||||
| 		} | ||||
|  | ||||
| 		foreach ($options['headers'] as $header) { | ||||
| 			if (!in_array($header, $params['headers'])) { | ||||
| 				throw new MissingHeaderError("$header was not a signed header"); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (isset($options['algorithms']) && !in_array($params['algorithm'], $options['algorithms'])) { | ||||
| 			throw new InvalidParamsError($params['algorithm'] . " is not a supported algorithm"); | ||||
| 		} | ||||
|  | ||||
| 		$date = null; | ||||
| 		if (isset($headers['date'])) { | ||||
| 			$date = strtotime($headers['date']); | ||||
| 		} elseif (isset($headers['x-date'])) { | ||||
| 			$date = strtotime($headers['x-date']); | ||||
| 		} | ||||
| 		if (!is_null($date)) { | ||||
| 			if ($date === FALSE) { | ||||
| 				throw new InvalidHeaderError('unable to parse date header'); | ||||
| 			} | ||||
| 			$skew = abs(time() - $date); | ||||
| 			if ($skew > $options['clockSkew']) { | ||||
| 				throw new ExpiredRequestError(sprintf("clock skew of %ds was greater than %ds", $skew, $options['clockSkew'])); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		$sign = array(); | ||||
| 		foreach ($params['headers'] as $header) { | ||||
| 			$sign[] = $header === 'request-line' ? $headers['request-line'] : sprintf("%s: %s", $header, $headers[$header]); | ||||
| 		} | ||||
|  | ||||
| 		return (array('scheme' => $scheme, 'params' => $params, 'signingString' => implode("\n", $sign))); | ||||
| 	} | ||||
|  | ||||
| 	static function verify(array $res, $key, $keytype) | ||||
| 	{ | ||||
| 		if (!is_string($key)) { | ||||
| 			throw new Exception('key is not a string'); | ||||
| 		} | ||||
|  | ||||
| 		$alg = explode('-', $res['params']['algorithm'], 2); | ||||
| 		if (sizeof($alg) != 2) { | ||||
| 			throw new InvalidAlgorithmError("unsupported algorithm"); | ||||
| 		} | ||||
| 		if ($alg[0] != $keytype) { | ||||
| 			throw new InvalidAlgorithmError("algorithm type doesn't match key type"); | ||||
| 		} | ||||
| 		switch ($alg[0]) { | ||||
| 		case 'rsa': | ||||
| 			$map = array('sha1' => OPENSSL_ALGO_SHA1, 'sha256' => OPENSSL_ALGO_SHA256, 'sha512' => OPENSSL_ALGO_SHA512); | ||||
| 			if (!array_key_exists($alg[1], $map)) { | ||||
| 				throw new InvalidAlgorithmError('unsupported algorithm'); | ||||
| 			} | ||||
| 			$pkey = openssl_get_publickey($key); | ||||
| 			if ($pkey === FALSE) { | ||||
| 				throw new Exception('key could not be parsed'); | ||||
| 			} | ||||
|  | ||||
| 			$rv = openssl_verify($res['signingString'], base64_decode($res['params']['signature']), $pkey, $map[$alg[1]]); | ||||
| 			openssl_free_key($pkey); | ||||
|  | ||||
| 			switch ($rv) { | ||||
| 			case 0: | ||||
| 				return (FALSE); | ||||
| 			case 1: | ||||
| 				return (TRUE); | ||||
| 			default: | ||||
| 				throw new Exception('key could not be verified'); | ||||
| 			} | ||||
| 			break; | ||||
|  | ||||
| 		case 'hmac': | ||||
| 			return (hash_hmac($alg[1], $res['signingString'], $key, true) === base64_decode($res['params']['signature'])); | ||||
| 			break; | ||||
| 		default: | ||||
| 			throw new InvalidAlgorithmError("unsupported algorithm"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static function sign(&$headers = array(), array $options = array()) | ||||
| 	{ | ||||
| 		if (is_null($headers)) { | ||||
| 			$headers = array(); | ||||
| 		} elseif (!is_array($headers)) { | ||||
| 			throw new Exception('headers are not an array'); | ||||
| 		} | ||||
|  | ||||
| 		if (!array_key_exists('keyId', $options)) { | ||||
| 			throw new Exception('keyId option is missing'); | ||||
| 		} elseif (!is_string($options['keyId'])) { | ||||
| 			throw new Exception('keyId option is not a string'); | ||||
| 		} | ||||
| 		if (!array_key_exists('key', $options)) { | ||||
| 			throw new Exception('key option is missing'); | ||||
| 		} elseif (!is_string($options['key'])) { | ||||
| 			throw new Exception('key option is not a string'); | ||||
| 		} | ||||
|  | ||||
| 		if (!array_key_exists('headers', $options)) { | ||||
| 			$options['headers'] = array('date'); | ||||
| 		} else { | ||||
| 			if (!is_array($options['headers'])) { | ||||
| 				throw new Exception('headers option is not an array'); | ||||
| 			} | ||||
| 			if (sizeof(array_filter($options['headers'], function ($a) { return (!is_string($a)); }))) { | ||||
| 				throw new Exception('headers option is not an array of strings'); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (!array_key_exists('algorithm', $options)) { | ||||
| 			$options['algorithm'] = 'rsa-sha256'; | ||||
| 		} | ||||
|  | ||||
| 		if (!array_key_exists('date', $headers)) { | ||||
| 			$headers['date'] = date(DATE_RFC1123); | ||||
| 		} | ||||
|  | ||||
| 		$headers['request-line'] = array_key_exists('requestLine', $options) ? | ||||
| 		    $options['requestLine'] : | ||||
| 		    sprintf("%s %s %s", $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']); | ||||
|  | ||||
| 		$sign = array(); | ||||
| 		foreach ($options['headers'] as $header) { | ||||
| 			if (!array_key_exists($header, $headers)) { | ||||
| 				throw new MissingHeaderError("$header was not in the request"); | ||||
| 			} | ||||
| 			$sign[] = $header === 'request-line' ? $headers['request-line'] : sprintf("%s: %s", $header, $headers[$header]); | ||||
| 		} | ||||
| 		$data = join("\n", $sign); | ||||
|  | ||||
| 		$alg = explode('-', $options['algorithm'], 2); | ||||
| 		if (sizeof($alg) != 2) { | ||||
| 			throw new InvalidAlgorithmError("unsupported algorithm"); | ||||
| 		} | ||||
| 		switch ($alg[0]) { | ||||
| 		case 'rsa': | ||||
| 			$map = array('sha1' => OPENSSL_ALGO_SHA1, 'sha256' => OPENSSL_ALGO_SHA256, 'sha512' => OPENSSL_ALGO_SHA512); | ||||
| 			if (!array_key_exists($alg[1], $map)) { | ||||
| 				throw new InvalidAlgorithmError('unsupported algorithm'); | ||||
| 			} | ||||
| 			$key = openssl_get_privatekey($options['key']); | ||||
| 			if ($key === FALSE) { | ||||
| 				error_log(openssl_error_string()); | ||||
| 				throw new Exception('key option could not be parsed'); | ||||
| 			} | ||||
|  | ||||
| 			if (openssl_sign($data, $signature, $key, $map[$alg[1]]) === FALSE) { | ||||
| 				throw new Exception('unable to sign'); | ||||
| 			} | ||||
| 			break; | ||||
|  | ||||
| 		case 'hmac': | ||||
| 			$signature = hash_hmac($alg[1], $data, $options['key'], true); | ||||
| 			break; | ||||
| 		default: | ||||
| 			throw new InvalidAlgorithmError("unsupported algorithm"); | ||||
| 		} | ||||
| 		unset($headers['request-line']); | ||||
| 		$headers['authorization'] = sprintf('Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"', | ||||
| 		    $options['keyId'], $options['algorithm'], implode(' ', $options['headers']), | ||||
| 		    base64_encode($signature)); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										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'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										270
									
								
								utils/postman.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										270
									
								
								utils/postman.php
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -20,7 +20,6 @@ | ||||
|  * @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/ | ||||
| @@ -29,6 +28,10 @@ if (!defined ('GNUSOCIAL')) { | ||||
|     exit(1); | ||||
| } | ||||
|  | ||||
| use GuzzleHttp\Client; | ||||
| use HttpSignatures\Context; | ||||
| use HttpSignatures\GuzzleHttpSignatures; | ||||
|  | ||||
| /** | ||||
|  * ActivityPub's own Postman | ||||
|  * | ||||
| @@ -44,53 +47,276 @@ if (!defined ('GNUSOCIAL')) { | ||||
| class Activitypub_postman | ||||
| { | ||||
|     private $actor; | ||||
|         private $to = array (); | ||||
|     private $actor_uri; | ||||
|     private $to = []; | ||||
|     private $client; | ||||
|     private $headers; | ||||
|  | ||||
|     /** | ||||
|      * 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 | ||||
|      * @param Array of Activitypub_profile $to destinataries | ||||
|      */ | ||||
|     public function __construct($from, $to) | ||||
|     { | ||||
|         $this->actor = $from; | ||||
|         $discovery = new Activitypub_explorer(); | ||||
|         $this->to = $to; | ||||
|                 $this->headers = array(); | ||||
|                 $this->headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; | ||||
|                 $this->headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; | ||||
|         $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() | ||||
|     { | ||||
|                 $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); | ||||
|         $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 Undo Follow notification to remote instance | ||||
|      * | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      */ | ||||
|     public function undo_follow() | ||||
|     { | ||||
|                 $this->client = new HTTPClient (); | ||||
|                 $data = array ("@context" => "https://www.w3.org/ns/activitystreams", | ||||
|                             "type"   => "Undo", | ||||
|                             "actor"  => $this->actor->getUrl (), | ||||
|                             "object" => array ( | ||||
|                                 "type" => "Follow", | ||||
|                                 "object" => $this->to[0]->getUrl () | ||||
|         $data = Activitypub_undo::undo_to_array( | ||||
|                     Activitypub_follow::follow_to_array( | ||||
|                         ActivityPubPlugin::actor_uri($this->actor), | ||||
|                         $this->to[0]->getUrl() | ||||
|                     ) | ||||
|                 ); | ||||
|                 $this->client->setBody (json_encode ($data)); | ||||
|                 $response = $this->client->post ($this->to[0]->getInbox (), $this->headers); | ||||
|         $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() | ||||
|                         ); | ||||
|         $data = json_encode($data, JSON_UNESCAPED_SLASHES); | ||||
|  | ||||
|         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