mirror of
https://github.com/ad-aures/castopod.git
synced 2026-04-02 14:29:11 +02:00
feat(fediverse): implement activitypub protocols + update user interface
- add "ActivityPub" library to handle server to server federation and basic
client to server protocols using activitypub:
- add webfinger endpoint to look for actor
- add actor definition with inbox / outbox / followers
- remote follow an actor
- create notes with possible preview cards
- interract with favourites, reblogs and replies
- block incoming actors and/or domains
- broadcast/schedule activities to fediverse followers using a cron task
- For castopod, the podcast is the actor:
- overwrite the activitypub library for castopod's specific needs
- perform basic interactions administrating a podcast to interact with fediverse users:
- create notes with episode attachment
- favourite and share a note + reply
- add specific castopod_namespaces for podcasts and episodes definitions
- overwrite CodeIgniter's Route service to include alternate-content option for
activitystream requests
- update episode publication logic:
- remove publication inputs in create / edit episode form
- publish / schedule or unpublish an episode after creation
- the podcaster publishes a note when publishing an episode
- Javascript / Typescript modules:
- fix Dropdown.ts to keep dropdown menu in foreground
- add Modal.ts for funding links modal
- add Toggler.ts to toggle various css states in ui
- User Interface:
- update tailwindcss to v2
- use castopod's pine and rose colors
- update public layout to a 3 column layout
- add pages in public for podcast activity, episode list and notes
- update episode page to include linked notes
- remove previous and next episodes from episode pages
- show different public views depending on whether user is authenticated or not
- use Kumbh Sans and Montserrat fonts
- update CodeIgniter's config files
- with CodeIgniter's new requirements, update docker environments are now based on
php v7.3 image
- move Image entity to Libraries
- update composer and npm packages to latest versions
closes #69 #65 #85, fixes #51 #91 #92 #88
This commit is contained in:
parent
dd3ac9b4ab
commit
2f525c0f6e
476 changed files with 26120 additions and 11160 deletions
|
|
@ -1,8 +1,8 @@
|
|||
FROM php:7.2-fpm
|
||||
FROM php:7.3-fpm
|
||||
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nodejs
|
||||
|
|
|
|||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -138,7 +138,6 @@ node_modules
|
|||
# public folder
|
||||
public/*
|
||||
!public/media
|
||||
!public/media/~person
|
||||
!public/.htaccess
|
||||
!public/favicon.ico
|
||||
!public/index.php
|
||||
|
|
@ -147,10 +146,14 @@ public/*
|
|||
# public media folder
|
||||
public/media/*
|
||||
!public/media/index.html
|
||||
!public/media/podcasts
|
||||
!public/media/persons
|
||||
|
||||
# public person folder
|
||||
public/media/~person/*
|
||||
!public/media/~person/index.html
|
||||
public/media/podcasts/*
|
||||
!public/media/podcasts/index.html
|
||||
|
||||
public/media/persons/*
|
||||
!public/media/persons/index.html
|
||||
|
||||
# Generated files
|
||||
app/Language/en/PersonsTaxonomy.php
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{
|
||||
"files": "*.php",
|
||||
"options": {
|
||||
"phpVersion": "7.2",
|
||||
"phpVersion": "7.3",
|
||||
"singleQuote": true
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
+ writable/***
|
||||
+ .env.example
|
||||
+ DEPENDENCIES.md
|
||||
+ LICENSE
|
||||
+ LICENSE.md
|
||||
+ README.md
|
||||
+ INSTALL.md
|
||||
- **
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"apply",
|
||||
"responsive",
|
||||
"variants",
|
||||
"screen"
|
||||
"screen",
|
||||
"layer"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
17
.svgo.icons.js
Normal file
17
.svgo.icons.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
"removeXMLNS",
|
||||
"removeDimensions",
|
||||
"sortAttrs",
|
||||
{
|
||||
name: "addAttributesToSVGElement",
|
||||
params: {
|
||||
attributes: [
|
||||
{ fill: "currentColor" },
|
||||
{ width: "1em" },
|
||||
{ height: "1em" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
plugins:
|
||||
- removeXMLNS: true
|
||||
- removeDimensions: true
|
||||
- addAttributesToSVGElement:
|
||||
attributes:
|
||||
- fill: currentColor
|
||||
- width: "1em"
|
||||
- height: "1em"
|
||||
- sortAttrs: true
|
||||
12
.svgo.js
Normal file
12
.svgo.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
name: "removeViewBox",
|
||||
active: false,
|
||||
},
|
||||
"removeXMLNS",
|
||||
"removeDimensions",
|
||||
"sortAttrs",
|
||||
"prefixIds",
|
||||
],
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
plugins:
|
||||
- removeXMLNS: true
|
||||
- removeDimensions: true
|
||||
- sortAttrs: true
|
||||
- prefixIds: true
|
||||
|
|
@ -4,7 +4,7 @@ Castopod uses the following components:
|
|||
|
||||
PHP Dependencies:
|
||||
|
||||
- [Code Igniter 4](https://codeigniter.com)
|
||||
- [CodeIgniter 4](https://codeigniter.com)
|
||||
([MIT License](https://codeigniter.com/user_guide/license.html))
|
||||
- [WhichBrowser/Parser-PHP](https://github.com/WhichBrowser/Parser-PHP)
|
||||
([MIT License](https://github.com/WhichBrowser/Parser-PHP/blob/master/LICENSE))
|
||||
|
|
@ -24,6 +24,14 @@ PHP Dependencies:
|
|||
([MIT License](https://github.com/podlibre/user-agents-php/blob/main/LICENSE))
|
||||
- [podlibre/ipcat](https://github.com/podlibre/ipcat)
|
||||
([GNU General Public License v3.0](https://github.com/podlibre/ipcat/blob/master/LICENSE))
|
||||
- [podlibre/podcast-namespace](https://code.podlibre.org/podlibre/podcastnamespace)
|
||||
([MIT License](https://code.podlibre.org/podlibre/podcastnamespace/-/blob/master/LICENSE))
|
||||
- [phpseclib](https://phpseclib.com/)
|
||||
([MIT License](https://github.com/phpseclib/phpseclib/blob/master/LICENSE))
|
||||
- [codeigniter4-uuid](https://github.com/michalsn/codeigniter4-uuid)
|
||||
([MIT License](https://github.com/michalsn/codeigniter4-uuid/blob/develop/LICENSE))
|
||||
- [essence](https://github.com/essence/essence)
|
||||
([The FreeBSD License](https://github.com/essence/essence/blob/master/LICENSE.txt))
|
||||
|
||||
Javascript dependencies:
|
||||
|
||||
|
|
@ -39,9 +47,15 @@ Javascript dependencies:
|
|||
([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
|
||||
- [flatpickr](https://flatpickr.js.org/)
|
||||
([MIT License](https://github.com/flatpickr/flatpickr/blob/master/LICENSE.md))
|
||||
- [popperjs](https://popper.js.org/)
|
||||
([MIT License](https://github.com/popperjs/popper-core/blob/master/LICENSE.md))
|
||||
|
||||
Other:
|
||||
|
||||
- [Kumbh Sans](https://fonts.google.com/specimen/Kumbh+Sans)
|
||||
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
|
||||
- [Montserrat](https://fonts.google.com/specimen/Montserrat)
|
||||
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
|
||||
- [RemixIcon](https://remixicon.com/)
|
||||
([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
|
||||
- [OPAWG/User agent list](https://github.com/opawg/user-agents)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM php:7.2-fpm
|
||||
FROM php:7.3-fpm
|
||||
|
||||
COPY . /castopod
|
||||
WORKDIR /castopod
|
||||
|
|
@ -25,3 +25,9 @@ RUN echo "file_uploads = On\n" \
|
|||
"post_max_size = 120M\n" \
|
||||
"max_execution_time = 300\n" \
|
||||
> /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
# install cron
|
||||
RUN apt-get update && \
|
||||
apt-get install -y cron
|
||||
|
||||
RUN crontab /castopod/crontab
|
||||
|
|
|
|||
25
INSTALL.md
25
INSTALL.md
|
|
@ -1,13 +1,16 @@
|
|||
# How to install Castopod
|
||||
# How to install Castopod <!-- omit in toc -->
|
||||
|
||||
Castopod was thought to be easy to install. Whether using dedicated or shared
|
||||
hosting, you can install it on most PHP-MySQL compatible web servers.
|
||||
|
||||
## Table of contents <!-- omit in toc -->
|
||||
|
||||
- [Install instructions](#install-instructions)
|
||||
- [(optional) Manual configuration](#optional-manual-configuration)
|
||||
- [Web Server Requirements](#web-server-requirements)
|
||||
- [PHP v7.2 or higher](#php-v72-or-higher)
|
||||
- [PHP v7.3 or higher](#php-v73-or-higher)
|
||||
- [MySQL compatible database](#mysql-compatible-database)
|
||||
- [Privileges](#privileges)
|
||||
- [(Optional) Other recommendations](#optional-other-recommendations)
|
||||
- [Security concerns](#security-concerns)
|
||||
|
||||
|
|
@ -19,9 +22,16 @@ hosting, you can install it on most PHP-MySQL compatible web servers.
|
|||
1. Download and unzip the Castopod package onto the web server if you haven’t
|
||||
already.
|
||||
- ⚠️ Set the web server document root to the `public/` sub-folder.
|
||||
2. Run the Castopod install script by going to the install wizard page
|
||||
2. ⚠️ For broadcasting social activities to the fediverse, add a cron task on
|
||||
your web server to run every minute (replace the paths accordingly):
|
||||
|
||||
```php
|
||||
* * * * * /path/to/php /path/to/castopod/public/index.php scheduled-activities
|
||||
```
|
||||
|
||||
3. Run the Castopod install script by going to the install wizard page
|
||||
(`https://your_domain_name.com/cp-install`) in your favorite web browser.
|
||||
3. Follow the instructions on your screen.
|
||||
4. Follow the instructions on your screen.
|
||||
|
||||
All done, start podcasting!
|
||||
|
||||
|
|
@ -36,13 +46,12 @@ Before uploading Castopod files to your web server:
|
|||
|
||||
## Web Server Requirements
|
||||
|
||||
### PHP v7.2 or higher
|
||||
### PHP v7.3 or higher
|
||||
|
||||
PHP version 7.2 or higher is required, with the following extensions installed:
|
||||
PHP version 7.3 or higher is required, with the following extensions installed:
|
||||
|
||||
- [intl](http://php.net/manual/en/intl.requirements.php)
|
||||
- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use
|
||||
the HTTP\CURLRequest library
|
||||
- [libcurl](http://php.net/manual/en/curl.requirements.php)
|
||||
- [mbstring](http://php.net/manual/en/mbstring.installation.php)
|
||||
|
||||
Additionally, make sure that the following extensions are enabled in your PHP:
|
||||
|
|
|
|||
661
LICENSE
661
LICENSE
|
|
@ -1,661 +0,0 @@
|
|||
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.
|
||||
|
||||
Castopod
|
||||
Copyright (C) 2020 Podlibre
|
||||
|
||||
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/>.
|
||||
598
LICENSE.md
Normal file
598
LICENSE.md
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
# GNU Affero General Public License
|
||||
|
||||
_Version 3, 19 November 2007_ _Copyright © 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/>>.
|
||||
12
README.md
12
README.md
|
|
@ -3,12 +3,12 @@
|
|||
Castopod is an open-source podcast hosting solution for everyone.\
|
||||
Whether you are a beginner, an amateur or a professional, you will get everything
|
||||
you need:\
|
||||
Create, upload, publish, and get comprehensive audience measurement that respects your
|
||||
listeners privacy.
|
||||
Create, upload, publish, and get comprehensive audience measurement that
|
||||
respects your listeners privacy.
|
||||
|
||||
Castopod is a free and open-source solution (AGPL v3).\
|
||||
Whether you choose to install it on your own server or have it hosted by a
|
||||
professional, all your data and analytics belong to you and you only.
|
||||
Whether you choose to install it on your own server or have it hosted by a professional,
|
||||
all your data and analytics belong to you and you only.
|
||||
|
||||

|
||||
|
||||
|
|
@ -18,7 +18,9 @@ Castopod can be hosted on any PHP/MySQL server:\
|
|||
Unzip it and you are ready to broadcast.
|
||||
|
||||
To install Castopod on your server:
|
||||
- Download [Castopod latest Package (zip or tar.gz)](https://code.podlibre.org/podlibre/castopod/-/releases),
|
||||
|
||||
- Download
|
||||
[Castopod latest Package (zip or tar.gz)](https://code.podlibre.org/podlibre/castopod/-/releases),
|
||||
- Follow the procedure “[How to install Castopod](./INSTALL.md)”.
|
||||
|
||||
## Documentation
|
||||
|
|
|
|||
14
app/Config/ActivityPub.php
Normal file
14
app/Config/ActivityPub.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php namespace Config;
|
||||
|
||||
use ActivityPub\Config\ActivityPub as ActivityPubBase;
|
||||
|
||||
class ActivityPub extends ActivityPubBase
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* ActivityPub Objects
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
public $actorObject = 'App\Libraries\PodcastActor';
|
||||
public $noteObject = 'App\Libraries\NoteObject';
|
||||
}
|
||||
|
|
@ -6,309 +6,487 @@ use CodeIgniter\Config\BaseConfig;
|
|||
|
||||
class App extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Base Site URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| URL to your CodeIgniter root. Typically this will be your base URL,
|
||||
| WITH a trailing slash:
|
||||
|
|
||||
| http://example.com/
|
||||
|
|
||||
| If this is not set then CodeIgniter will try guess the protocol, domain
|
||||
| and path to your installation. However, you should always configure this
|
||||
| explicitly and never rely on auto-guessing, especially in production
|
||||
| environments.
|
||||
|
|
||||
*/
|
||||
public $baseURL = 'http://127.0.0.1:8080/';
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Base Site URL
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* URL to your CodeIgniter root. Typically this will be your base URL,
|
||||
* WITH a trailing slash:
|
||||
*
|
||||
* http://example.com/
|
||||
*
|
||||
* If this is not set then CodeIgniter will try guess the protocol, domain
|
||||
* and path to your installation. However, you should always configure this
|
||||
* explicitly and never rely on auto-guessing, especially in production
|
||||
* environments.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $baseURL = 'http://localhost:8080/';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Media Base URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| URL to your media root. Typically this will be your base URL,
|
||||
| WITH a trailing slash:
|
||||
|
|
||||
| http://cdn.example.com/
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Media Base URL
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* URL to your media root. Typically this will be your base URL,
|
||||
* WITH a trailing slash:
|
||||
*
|
||||
* http://cdn.example.com/
|
||||
*/
|
||||
public $mediaBaseURL = 'http://127.0.0.2:8080/';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index File
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Typically this will be your index.php file, unless you've renamed it to
|
||||
| something else. If you are using mod_rewrite to remove the page set this
|
||||
| variable so that it is blank.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Index File
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Typically this will be your index.php file, unless you've renamed it to
|
||||
* something else. If you are using mod_rewrite to remove the page set this
|
||||
* variable so that it is blank.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $indexPage = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| URI PROTOCOL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This item determines which getServer global should be used to retrieve the
|
||||
| URI string. The default setting of 'REQUEST_URI' works for most servers.
|
||||
| If your links do not seem to work, try one of the other delicious flavors:
|
||||
|
|
||||
| 'REQUEST_URI' Uses $_SERVER['REQUEST_URI']
|
||||
| 'QUERY_STRING' Uses $_SERVER['QUERY_STRING']
|
||||
| 'PATH_INFO' Uses $_SERVER['PATH_INFO']
|
||||
|
|
||||
| WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* URI PROTOCOL
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This item determines which getServer global should be used to retrieve the
|
||||
* URI string. The default setting of 'REQUEST_URI' works for most servers.
|
||||
* If your links do not seem to work, try one of the other delicious flavors:
|
||||
*
|
||||
* 'REQUEST_URI' Uses $_SERVER['REQUEST_URI']
|
||||
* 'QUERY_STRING' Uses $_SERVER['QUERY_STRING']
|
||||
* 'PATH_INFO' Uses $_SERVER['PATH_INFO']
|
||||
*
|
||||
* WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $uriProtocol = 'REQUEST_URI';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Locale
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Locale roughly represents the language and location that your visitor
|
||||
| is viewing the site from. It affects the language strings and other
|
||||
| strings (like currency markers, numbers, etc), that your program
|
||||
| should run under for this request.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Default Locale
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The Locale roughly represents the language and location that your visitor
|
||||
* is viewing the site from. It affects the language strings and other
|
||||
* strings (like currency markers, numbers, etc), that your program
|
||||
* should run under for this request.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $defaultLocale = 'en';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Negotiate Locale
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If true, the current Request object will automatically determine the
|
||||
| language to use based on the value of the Accept-Language header.
|
||||
|
|
||||
| If false, no automatic detection will be performed.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Negotiate Locale
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If true, the current Request object will automatically determine the
|
||||
* language to use based on the value of the Accept-Language header.
|
||||
*
|
||||
* If false, no automatic detection will be performed.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $negotiateLocale = true;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Supported Locales
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If $negotiateLocale is true, this array lists the locales supported
|
||||
| by the application in descending order of priority. If no match is
|
||||
| found, the first locale will be used.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Supported Locales
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If $negotiateLocale is true, this array lists the locales supported
|
||||
* by the application in descending order of priority. If no match is
|
||||
* found, the first locale will be used.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $supportedLocales = ['en', 'fr'];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default timezone that will be used in your application to display
|
||||
| dates with the date helper, and can be retrieved through app_timezone()
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Application Timezone
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The default timezone that will be used in your application to display
|
||||
* dates with the date helper, and can be retrieved through app_timezone()
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $appTimezone = 'UTC';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Character Set
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This determines which character set is used by default in various methods
|
||||
| that require a character set to be provided.
|
||||
|
|
||||
| See http://php.net/htmlspecialchars for a list of supported charsets.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Default Character Set
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This determines which character set is used by default in various methods
|
||||
* that require a character set to be provided.
|
||||
*
|
||||
* @see http://php.net/htmlspecialchars for a list of supported charsets.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $charset = 'UTF-8';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| URI PROTOCOL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If true, this will force every request made to this application to be
|
||||
| made via a secure connection (HTTPS). If the incoming request is not
|
||||
| secure, the user will be redirected to a secure version of the page
|
||||
| and the HTTP Strict Transport Security header will be set.
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* URI PROTOCOL
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If true, this will force every request made to this application to be
|
||||
* made via a secure connection (HTTPS). If the incoming request is not
|
||||
* secure, the user will be redirected to a secure version of the page
|
||||
* and the HTTP Strict Transport Security header will be set.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $forceGlobalSecureRequests = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Variables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 'sessionDriver'
|
||||
|
|
||||
| The storage driver to use: files, database, redis, memcached
|
||||
| - CodeIgniter\Session\Handlers\FileHandler
|
||||
| - CodeIgniter\Session\Handlers\DatabaseHandler
|
||||
| - CodeIgniter\Session\Handlers\MemcachedHandler
|
||||
| - CodeIgniter\Session\Handlers\RedisHandler
|
||||
|
|
||||
| 'sessionCookieName'
|
||||
|
|
||||
| The session cookie name, must contain only [0-9a-z_-] characters
|
||||
|
|
||||
| 'sessionExpiration'
|
||||
|
|
||||
| The number of SECONDS you want the session to last.
|
||||
| Setting to 0 (zero) means expire when the browser is closed.
|
||||
|
|
||||
| 'sessionSavePath'
|
||||
|
|
||||
| The location to save sessions to, driver dependent.
|
||||
|
|
||||
| For the 'files' driver, it's a path to a writable directory.
|
||||
| WARNING: Only absolute paths are supported!
|
||||
|
|
||||
| For the 'database' driver, it's a table name.
|
||||
| Please read up the manual for the format with other session drivers.
|
||||
|
|
||||
| IMPORTANT: You are REQUIRED to set a valid save path!
|
||||
|
|
||||
| 'sessionMatchIP'
|
||||
|
|
||||
| Whether to match the user's IP address when reading the session data.
|
||||
|
|
||||
| WARNING: If you're using the database driver, don't forget to update
|
||||
| your session table's PRIMARY KEY when changing this setting.
|
||||
|
|
||||
| 'sessionTimeToUpdate'
|
||||
|
|
||||
| How many seconds between CI regenerating the session ID.
|
||||
|
|
||||
| 'sessionRegenerateDestroy'
|
||||
|
|
||||
| Whether to destroy session data associated with the old session ID
|
||||
| when auto-regenerating the session ID. When set to FALSE, the data
|
||||
| will be later deleted by the garbage collector.
|
||||
|
|
||||
| Other session cookie settings are shared with the rest of the application,
|
||||
| except for 'cookie_prefix' and 'cookie_httponly', which are ignored here.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Session Driver
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The session storage driver to use:
|
||||
* - `CodeIgniter\Session\Handlers\FileHandler`
|
||||
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
|
||||
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
|
||||
* - `CodeIgniter\Session\Handlers\RedisHandler`
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Session Cookie Name
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The session cookie name, must contain only [0-9a-z_-] characters
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sessionCookieName = 'ci_session';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Session Expiration
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The number of SECONDS you want the session to last.
|
||||
* Setting to 0 (zero) means expire when the browser is closed.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $sessionExpiration = 7200;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Session Save Path
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The location to save sessions to and is driver dependent.
|
||||
*
|
||||
* For the 'files' driver, it's a path to a writable directory.
|
||||
* WARNING: Only absolute paths are supported!
|
||||
*
|
||||
* For the 'database' driver, it's a table name.
|
||||
* Please read up the manual for the format with other session drivers.
|
||||
*
|
||||
* IMPORTANT: You are REQUIRED to set a valid save path!
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sessionSavePath = WRITEPATH . 'session';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Session Match IP
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Whether to match the user's IP address when reading the session data.
|
||||
*
|
||||
* WARNING: If you're using the database driver, don't forget to update
|
||||
* your session table's PRIMARY KEY when changing this setting.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $sessionMatchIP = false;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Session Time to Update
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* How many seconds between CI regenerating the session ID.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $sessionTimeToUpdate = 300;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Session Regenerate Destroy
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Whether to destroy session data associated with the old session ID
|
||||
* when auto-regenerating the session ID. When set to FALSE, the data
|
||||
* will be later deleted by the garbage collector.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $sessionRegenerateDestroy = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cookie Related Variables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 'cookiePrefix' = Set a cookie name prefix if you need to avoid collisions
|
||||
| 'cookieDomain' = Set to .your-domain.com for site-wide cookies
|
||||
| 'cookiePath' = Typically will be a forward slash
|
||||
| 'cookieSecure' = Cookie will only be set if a secure HTTPS connection exists.
|
||||
| 'cookieHTTPOnly' = Cookie will only be accessible via HTTP(S) (no javascript)
|
||||
|
|
||||
| Note: These settings (with the exception of 'cookie_prefix' and
|
||||
| 'cookie_httponly') will also affect sessions.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cookie Prefix
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Set a cookie name prefix if you need to avoid collisions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $cookiePrefix = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cookie Domain
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Set to `.your-domain.com` for site-wide cookies.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $cookieDomain = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cookie Path
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Typically will be a forward slash.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $cookiePath = '/';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cookie Secure
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Cookie will only be set if a secure HTTPS connection exists.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $cookieSecure = false;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cookie HTTP Only
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Cookie will only be accessible via HTTP(S) (no JavaScript).
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $cookieHTTPOnly = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverse Proxy IPs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If your server is behind a reverse proxy, you must whitelist the proxy
|
||||
| IP addresses from which CodeIgniter should trust headers such as
|
||||
| HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
|
||||
| the visitor's IP address.
|
||||
|
|
||||
| You can use both an array or a comma-separated list of proxy addresses,
|
||||
| as well as specifying whole subnets. Here are a few examples:
|
||||
|
|
||||
| Comma-separated: '10.0.1.200,192.168.5.0/24'
|
||||
| Array: array('10.0.1.200', '192.168.5.0/24')
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cookie SameSite
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Configure cookie SameSite setting. Allowed values are:
|
||||
* - None
|
||||
* - Lax
|
||||
* - Strict
|
||||
* - ''
|
||||
*
|
||||
* Defaults to `Lax` for compatibility with modern browsers. Setting `''`
|
||||
* (empty string) means no SameSite attribute will be set on cookies. If
|
||||
* set to `None`, `$cookieSecure` must also be set.
|
||||
*
|
||||
* @var string 'Lax'|'None'|'Strict'
|
||||
*/
|
||||
public $cookieSameSite = 'Lax';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Reverse Proxy IPs
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If your server is behind a reverse proxy, you must whitelist the proxy
|
||||
* IP addresses from which CodeIgniter should trust headers such as
|
||||
* HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
|
||||
* the visitor's IP address.
|
||||
*
|
||||
* You can use both an array or a comma-separated list of proxy addresses,
|
||||
* as well as specifying whole subnets. Here are a few examples:
|
||||
*
|
||||
* Comma-separated: '10.0.1.200,192.168.5.0/24'
|
||||
* Array: ['10.0.1.200', '192.168.5.0/24']
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $proxyIPs = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cross Site Request Forgery
|
||||
|--------------------------------------------------------------------------
|
||||
| Enables a CSRF cookie token to be set. When set to TRUE, token will be
|
||||
| checked on a submitted form. If you are accepting user data, it is strongly
|
||||
| recommended CSRF protection be enabled.
|
||||
|
|
||||
| CSRFTokenName = The token name
|
||||
| CSRFHeaderName = The header name
|
||||
| CSRFCookieName = The cookie name
|
||||
| CSRFExpire = The number in seconds the token should expire.
|
||||
| CSRFRegenerate = Regenerate token on every submission
|
||||
| CSRFRedirect = Redirect to previous page with error on failure
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Token Name
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The token name.
|
||||
*
|
||||
* @deprecated Use `Config\Security` $tokenName property instead of using this property.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $CSRFTokenName = 'csrf_test_name';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Header Name
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The header name.
|
||||
*
|
||||
* @deprecated Use `Config\Security` $headerName property instead of using this property.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $CSRFHeaderName = 'X-CSRF-TOKEN';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Cookie Name
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The cookie name.
|
||||
*
|
||||
* @deprecated Use `Config\Security` $cookieName property instead of using this property.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $CSRFCookieName = 'csrf_cookie_name';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Expire
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The number in seconds the token should expire.
|
||||
*
|
||||
* @deprecated Use `Config\Security` $expire property instead of using this property.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $CSRFExpire = 7200;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Regenerate
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Regenerate token on every submission?
|
||||
*
|
||||
* @deprecated Use `Config\Security` $regenerate property instead of using this property.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $CSRFRegenerate = true;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Redirect
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Redirect to previous page with error on failure?
|
||||
*
|
||||
* @deprecated Use `Config\Security` $redirect property instead of using this property.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $CSRFRedirect = true;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Content Security Policy
|
||||
|--------------------------------------------------------------------------
|
||||
| Enables the Response's Content Secure Policy to restrict the sources that
|
||||
| can be used for images, scripts, CSS files, audio, video, etc. If enabled,
|
||||
| the Response object will populate default values for the policy from the
|
||||
| ContentSecurityPolicy.php file. Controllers can always add to those
|
||||
| restrictions at run time.
|
||||
|
|
||||
| For a better understanding of CSP, see these documents:
|
||||
| - http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
||||
| - http://www.w3.org/TR/CSP/
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF SameSite
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Setting for CSRF SameSite cookie token. Allowed values are:
|
||||
* - None
|
||||
* - Lax
|
||||
* - Strict
|
||||
* - ''
|
||||
*
|
||||
* Defaults to `Lax` as recommended in this link:
|
||||
*
|
||||
* @see https://portswigger.net/web-security/csrf/samesite-cookies
|
||||
*
|
||||
* @deprecated Use `Config\Security` $samesite property instead of using this property.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $CSRFSameSite = 'Lax';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Content Security Policy
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Enables the Response's Content Secure Policy to restrict the sources that
|
||||
* can be used for images, scripts, CSS files, audio, video, etc. If enabled,
|
||||
* the Response object will populate default values for the policy from the
|
||||
* `ContentSecurityPolicy.php` file. Controllers can always add to those
|
||||
* restrictions at run time.
|
||||
*
|
||||
* For a better understanding of CSP, see these documents:
|
||||
*
|
||||
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
||||
* @see http://www.w3.org/TR/CSP/
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $CSPEnabled = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Media root folder
|
||||
|--------------------------------------------------------------------------
|
||||
| Defines the root folder for media files storage
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Media root folder
|
||||
* --------------------------------------------------------------------------
|
||||
* Defines the root folder for media files storage
|
||||
*/
|
||||
public $mediaRoot = 'media';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Admin gateway
|
||||
|--------------------------------------------------------------------------
|
||||
| Defines a base route for all admin pages
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Admin gateway
|
||||
* --------------------------------------------------------------------------
|
||||
* Defines a base route for all admin pages
|
||||
*/
|
||||
public $adminGateway = 'cp-admin';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auth gateway
|
||||
|--------------------------------------------------------------------------
|
||||
| Defines a base route for all authentication related pages
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Auth gateway
|
||||
* --------------------------------------------------------------------------
|
||||
* Defines a base route for all authentication related pages
|
||||
*/
|
||||
public $authGateway = 'cp-auth';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Install gateway
|
||||
|--------------------------------------------------------------------------
|
||||
| Defines a base route for instance installation
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Install gateway
|
||||
* --------------------------------------------------------------------------
|
||||
* Defines a base route for instance installation
|
||||
*/
|
||||
public $installGateway = 'cp-install';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,90 +2,66 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
require_once SYSTEMPATH . 'Config/AutoloadConfig.php';
|
||||
use CodeIgniter\Config\AutoloadConfig;
|
||||
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* AUTO-LOADER
|
||||
* -------------------------------------------------------------------
|
||||
*
|
||||
* This file defines the namespaces and class maps so the Autoloader
|
||||
* can find the files as needed.
|
||||
*
|
||||
* NOTE: If you use an identical key in $psr4 or $classmap, then
|
||||
* the values in this file will overwrite the framework's values.
|
||||
*/
|
||||
class Autoload extends \CodeIgniter\Config\AutoloadConfig
|
||||
class Autoload extends AutoloadConfig
|
||||
{
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Namespaces
|
||||
* -------------------------------------------------------------------
|
||||
* This maps the locations of any namespaces in your application to
|
||||
* their location on the file system. These are used by the autoloader
|
||||
* to locate files the first time they have been instantiated.
|
||||
*
|
||||
* The '/app' and '/system' directories are already mapped for you.
|
||||
* you may change the name of the 'App' namespace if you wish,
|
||||
* but this should be done prior to creating any namespaced classes,
|
||||
* else you will need to modify all of those classes for this to work.
|
||||
*
|
||||
* Prototype:
|
||||
*
|
||||
* $psr4 = [
|
||||
* 'CodeIgniter' => SYSTEMPATH,
|
||||
* 'App' => APPPATH
|
||||
* ];
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $psr4 = [
|
||||
'App' => APPPATH,
|
||||
APP_NAMESPACE => APPPATH, // For custom app namespace
|
||||
'Config' => APPPATH . 'Config',
|
||||
'ActivityPub' => APPPATH . 'Libraries/ActivityPub',
|
||||
];
|
||||
|
||||
public $classmap = [];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Collects the application-specific autoload settings and merges
|
||||
* them with the framework's required settings.
|
||||
* -------------------------------------------------------------------
|
||||
* Class Map
|
||||
* -------------------------------------------------------------------
|
||||
* The class map provides a map of class names and their exact
|
||||
* location on the drive. Classes loaded in this manner will have
|
||||
* slightly faster performance because they will not have to be
|
||||
* searched for within one or more directories as they would if they
|
||||
* were being autoloaded through a namespace.
|
||||
*
|
||||
* NOTE: If you use an identical key in $psr4 or $classmap, then
|
||||
* the values in this file will overwrite the framework's values.
|
||||
* Prototype:
|
||||
*
|
||||
* $classmap = [
|
||||
* 'MyClass' => '/path/to/class/file.php'
|
||||
* ];
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Namespaces
|
||||
* -------------------------------------------------------------------
|
||||
* This maps the locations of any namespaces in your application
|
||||
* to their location on the file system. These are used by the
|
||||
* Autoloader to locate files the first time they have been instantiated.
|
||||
*
|
||||
* The '/app' and '/system' directories are already mapped for
|
||||
* you. You may change the name of the 'App' namespace if you wish,
|
||||
* but this should be done prior to creating any namespaced classes,
|
||||
* else you will need to modify all of those classes for this to work.
|
||||
*
|
||||
* DO NOT change the name of the CodeIgniter namespace or your application
|
||||
* WILL break. *
|
||||
* Prototype:
|
||||
*
|
||||
* $Config['psr4'] = [
|
||||
* 'CodeIgniter' => SYSPATH
|
||||
* `];
|
||||
*/
|
||||
$psr4 = [
|
||||
'App' => APPPATH, // To ensure filters, etc still found,
|
||||
APP_NAMESPACE => APPPATH, // For custom namespace
|
||||
'Config' => APPPATH . 'Config',
|
||||
];
|
||||
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Class Map
|
||||
* -------------------------------------------------------------------
|
||||
* The class map provides a map of class names and their exact
|
||||
* location on the drive. Classes loaded in this manner will have
|
||||
* slightly faster performance because they will not have to be
|
||||
* searched for within one or more directories as they would if they
|
||||
* were being autoloaded through a namespace.
|
||||
*
|
||||
* Prototype:
|
||||
*
|
||||
* $Config['classmap'] = [
|
||||
* 'MyClass' => '/path/to/class/file.php'
|
||||
* ];
|
||||
*/
|
||||
$classmap = [];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Do Not Edit Below This Line
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
$this->psr4 = array_merge($this->psr4, $psr4);
|
||||
$this->classmap = array_merge($this->classmap, $classmap);
|
||||
|
||||
unset($psr4, $classmap);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
public $classmap = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,32 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ERROR DISPLAY
|
||||
|--------------------------------------------------------------------------
|
||||
| In development, we want to show as many errors as possible to help
|
||||
| make sure they don't make it to production. And save us hours of
|
||||
| painful debugging.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* ERROR DISPLAY
|
||||
* --------------------------------------------------------------------------
|
||||
* In development, we want to show as many errors as possible to help
|
||||
* make sure they don't make it to production. And save us hours of
|
||||
* painful debugging.
|
||||
*/
|
||||
error_reporting(-1);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG BACKTRACES
|
||||
|--------------------------------------------------------------------------
|
||||
| If true, this constant will tell the error screens to display debug
|
||||
| backtraces along with the other error information. If you would
|
||||
| prefer to not see this, set this value to false.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* DEBUG BACKTRACES
|
||||
* --------------------------------------------------------------------------
|
||||
* If true, this constant will tell the error screens to display debug
|
||||
* backtraces along with the other error information. If you would
|
||||
* prefer to not see this, set this value to false.
|
||||
*/
|
||||
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG MODE
|
||||
|--------------------------------------------------------------------------
|
||||
| Debug mode is an experimental flag that can allow changes throughout
|
||||
| the system. This will control whether Kint is loaded, and a few other
|
||||
| items. It can always be used within your own application too.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* DEBUG MODE
|
||||
* --------------------------------------------------------------------------
|
||||
* Debug mode is an experimental flag that can allow changes throughout
|
||||
* the system. This will control whether Kint is loaded, and a few other
|
||||
* items. It can always be used within your own application too.
|
||||
*/
|
||||
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', 1);
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ERROR DISPLAY
|
||||
|--------------------------------------------------------------------------
|
||||
| Don't show ANY in production environments. Instead, let the system catch
|
||||
| it and display a generic error message.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* ERROR DISPLAY
|
||||
* --------------------------------------------------------------------------
|
||||
* Don't show ANY in production environments. Instead, let the system catch
|
||||
* it and display a generic error message.
|
||||
*/
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(
|
||||
|
|
@ -14,16 +14,15 @@ error_reporting(
|
|||
~E_DEPRECATED &
|
||||
~E_STRICT &
|
||||
~E_USER_NOTICE &
|
||||
~E_USER_DEPRECATED
|
||||
~E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG MODE
|
||||
|--------------------------------------------------------------------------
|
||||
| Debug mode is an experimental flag that can allow changes throughout
|
||||
| the system. It's not widely used currently, and may not survive
|
||||
| release of the framework.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* DEBUG MODE
|
||||
* --------------------------------------------------------------------------
|
||||
* Debug mode is an experimental flag that can allow changes throughout
|
||||
* the system. It's not widely used currently, and may not survive
|
||||
* release of the framework.
|
||||
*/
|
||||
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', 0);
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', false);
|
||||
|
|
|
|||
|
|
@ -1,33 +1,32 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ERROR DISPLAY
|
||||
|--------------------------------------------------------------------------
|
||||
| In development, we want to show as many errors as possible to help
|
||||
| make sure they don't make it to production. And save us hours of
|
||||
| painful debugging.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* ERROR DISPLAY
|
||||
* --------------------------------------------------------------------------
|
||||
* In development, we want to show as many errors as possible to help
|
||||
* make sure they don't make it to production. And save us hours of
|
||||
* painful debugging.
|
||||
*/
|
||||
error_reporting(-1);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG BACKTRACES
|
||||
|--------------------------------------------------------------------------
|
||||
| If true, this constant will tell the error screens to display debug
|
||||
| backtraces along with the other error information. If you would
|
||||
| prefer to not see this, set this value to false.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* DEBUG BACKTRACES
|
||||
* --------------------------------------------------------------------------
|
||||
* If true, this constant will tell the error screens to display debug
|
||||
* backtraces along with the other error information. If you would
|
||||
* prefer to not see this, set this value to false.
|
||||
*/
|
||||
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG MODE
|
||||
|--------------------------------------------------------------------------
|
||||
| Debug mode is an experimental flag that can allow changes throughout
|
||||
| the system. It's not widely used currently, and may not survive
|
||||
| release of the framework.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* DEBUG MODE
|
||||
* --------------------------------------------------------------------------
|
||||
* Debug mode is an experimental flag that can allow changes throughout
|
||||
* the system. It's not widely used currently, and may not survive
|
||||
* release of the framework.
|
||||
*/
|
||||
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', 1);
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||
|
|
|
|||
|
|
@ -2,83 +2,111 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Cache\Handlers\DummyHandler;
|
||||
use CodeIgniter\Cache\Handlers\FileHandler;
|
||||
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
||||
use CodeIgniter\Cache\Handlers\PredisHandler;
|
||||
use CodeIgniter\Cache\Handlers\RedisHandler;
|
||||
use CodeIgniter\Cache\Handlers\WincacheHandler;
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class Cache extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Primary Handler
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The name of the preferred handler that should be used. If for some reason
|
||||
| it is not available, the $backupHandler will be used in its place.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Primary Handler
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The name of the preferred handler that should be used. If for some reason
|
||||
* it is not available, the $backupHandler will be used in its place.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $handler = 'file';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Backup Handler
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The name of the handler that will be used in case the first one is
|
||||
| unreachable. Often, 'file' is used here since the filesystem is
|
||||
| always available, though that's not always practical for the app.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Backup Handler
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The name of the handler that will be used in case the first one is
|
||||
* unreachable. Often, 'file' is used here since the filesystem is
|
||||
* always available, though that's not always practical for the app.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $backupHandler = 'dummy';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Directory Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The path to where cache files should be stored, if using a file-based
|
||||
| system.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cache Directory Path
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The path to where cache files should be stored, if using a file-based
|
||||
* system.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @deprecated Use the driver-specific variant under $file
|
||||
*/
|
||||
public $storePath = WRITEPATH . 'cache/';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Include Query String
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Whether to take the URL query string into consideration when generating
|
||||
| output cache files. Valid options are:
|
||||
|
|
||||
| false = Disabled
|
||||
| true = Enabled, take all query parameters into account.
|
||||
| Please be aware that this may result in numerous cache
|
||||
| files generated for the same page over and over again.
|
||||
| array('q') = Enabled, but only take into account the specified list
|
||||
| of query parameters.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Cache Include Query String
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Whether to take the URL query string into consideration when generating
|
||||
* output cache files. Valid options are:
|
||||
*
|
||||
* false = Disabled
|
||||
* true = Enabled, take all query parameters into account.
|
||||
* Please be aware that this may result in numerous cache
|
||||
* files generated for the same page over and over again.
|
||||
* array('q') = Enabled, but only take into account the specified list
|
||||
* of query parameters.
|
||||
*
|
||||
* @var boolean|string[]
|
||||
*/
|
||||
public $cacheQueryString = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This string is added to all cache item names to help avoid collisions
|
||||
| if you run multiple applications with the same cache engine.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Key Prefix
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This string is added to all cache item names to help avoid collisions
|
||||
* if you run multiple applications with the same cache engine.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $prefix = '';
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------------
|
||||
| Memcached settings
|
||||
| -------------------------------------------------------------------------
|
||||
| Your Memcached servers can be specified below, if you are using
|
||||
| the Memcached drivers.
|
||||
|
|
||||
| See: https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* File settings
|
||||
* --------------------------------------------------------------------------
|
||||
* Your file storage preferences can be specified below, if you are using
|
||||
* the File driver.
|
||||
*
|
||||
* @var array<string, string|int|null>
|
||||
*/
|
||||
public $file = [
|
||||
'storePath' => WRITEPATH . 'cache/',
|
||||
'mode' => 0640,
|
||||
];
|
||||
|
||||
/**
|
||||
* -------------------------------------------------------------------------
|
||||
* Memcached settings
|
||||
* -------------------------------------------------------------------------
|
||||
* Your Memcached servers can be specified below, if you are using
|
||||
* the Memcached drivers.
|
||||
*
|
||||
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
||||
*
|
||||
* @var array<string, string|int|boolean>
|
||||
*/
|
||||
public $memcached = [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 11211,
|
||||
|
|
@ -86,14 +114,15 @@ class Cache extends BaseConfig
|
|||
'raw' => false,
|
||||
];
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------------
|
||||
| Redis settings
|
||||
| -------------------------------------------------------------------------
|
||||
| Your Redis server can be specified below, if you are using
|
||||
| the Redis or Predis drivers.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* -------------------------------------------------------------------------
|
||||
* Redis settings
|
||||
* -------------------------------------------------------------------------
|
||||
* Your Redis server can be specified below, if you are using
|
||||
* the Redis or Predis drivers.
|
||||
*
|
||||
* @var array<string, string|int|null>
|
||||
*/
|
||||
public $redis = [
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
|
|
@ -102,21 +131,22 @@ class Cache extends BaseConfig
|
|||
'database' => 0,
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available Cache Handlers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is an array of cache engine alias' and class names. Only engines
|
||||
| that are listed here are allowed to be used.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Available Cache Handlers
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This is an array of cache engine alias' and class names. Only engines
|
||||
* that are listed here are allowed to be used.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $validHandlers = [
|
||||
'dummy' => \CodeIgniter\Cache\Handlers\DummyHandler::class,
|
||||
'file' => \CodeIgniter\Cache\Handlers\FileHandler::class,
|
||||
'memcached' => \CodeIgniter\Cache\Handlers\MemcachedHandler::class,
|
||||
'predis' => \CodeIgniter\Cache\Handlers\PredisHandler::class,
|
||||
'redis' => \CodeIgniter\Cache\Handlers\RedisHandler::class,
|
||||
'wincache' => \CodeIgniter\Cache\Handlers\WincacheHandler::class,
|
||||
'dummy' => DummyHandler::class,
|
||||
'file' => FileHandler::class,
|
||||
'memcached' => MemcachedHandler::class,
|
||||
'predis' => PredisHandler::class,
|
||||
'redis' => RedisHandler::class,
|
||||
'wincache' => WincacheHandler::class,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,50 @@
|
|||
<?php
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Castopod Version
|
||||
//--------------------------------------------------------------------------
|
||||
// The Castopod version number to display.
|
||||
//
|
||||
// NOTE: this constant is updated upon release with Continuous Integration.
|
||||
//
|
||||
/*
|
||||
| --------------------------------------------------------------------
|
||||
| Castopod Version
|
||||
| --------------------------------------------------------------------
|
||||
|
|
||||
| The Castopod version number to display.
|
||||
|
|
||||
| NOTE: this constant is updated upon release with Continuous Integration.
|
||||
*/
|
||||
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.41');
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// App Namespace
|
||||
//--------------------------------------------------------------------
|
||||
// This defines the default Namespace that is used throughout
|
||||
// CodeIgniter to refer to the Application directory. Change
|
||||
// this constant to change the namespace that all application
|
||||
// classes should use.
|
||||
//
|
||||
// NOTE: changing this will require manually modifying the
|
||||
// existing namespaces of App\* namespaced-classes.
|
||||
//
|
||||
/*
|
||||
| --------------------------------------------------------------------
|
||||
| App Namespace
|
||||
| --------------------------------------------------------------------
|
||||
|
|
||||
| This defines the default Namespace that is used throughout
|
||||
| CodeIgniter to refer to the Application directory. Change
|
||||
| this constant to change the namespace that all application
|
||||
| classes should use.
|
||||
|
|
||||
| NOTE: changing this will require manually modifying the
|
||||
| existing namespaces of App\* namespaced-classes.
|
||||
*/
|
||||
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Composer Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The path that Composer's autoload file is expected to live. By default,
|
||||
| the vendor folder is in the Root directory, but you can customize that here.
|
||||
*/
|
||||
| --------------------------------------------------------------------------
|
||||
| Composer Path
|
||||
| --------------------------------------------------------------------------
|
||||
|
|
||||
| The path that Composer's autoload file is expected to live. By default,
|
||||
| the vendor folder is in the Root directory, but you can customize that here.
|
||||
*/
|
||||
defined('COMPOSER_PATH') ||
|
||||
define('COMPOSER_PATH', ROOTPATH . 'vendor/autoload.php');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Timing Constants
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Provide simple ways to work with the myriad of PHP functions that
|
||||
| require information to be in seconds.
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Timing Constants
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Provide simple ways to work with the myriad of PHP functions that
|
||||
| require information to be in seconds.
|
||||
*/
|
||||
defined('SECOND') || define('SECOND', 1);
|
||||
defined('MINUTE') || define('MINUTE', 60);
|
||||
defined('HOUR') || define('HOUR', 3600);
|
||||
|
|
@ -51,30 +55,30 @@ defined('YEAR') || define('YEAR', 31536000);
|
|||
defined('DECADE') || define('DECADE', 315360000);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Exit Status Codes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Used to indicate the conditions under which the script is exit()ing.
|
||||
| While there is no universal standard for error codes, there are some
|
||||
| broad conventions. Three such conventions are mentioned below, for
|
||||
| those who wish to make use of them. The CodeIgniter defaults were
|
||||
| chosen for the least overlap with these conventions, while still
|
||||
| leaving room for others to be defined in future versions and user
|
||||
| applications.
|
||||
|
|
||||
| The three main conventions used for determining exit status codes
|
||||
| are as follows:
|
||||
|
|
||||
| Standard C/C++ Library (stdlibc):
|
||||
| http://www.gnu.org/software/libc/manual/html_node/Exit-Status.html
|
||||
| (This link also contains other GNU-specific conventions)
|
||||
| BSD sysexits.h:
|
||||
| http://www.gsp.com/cgi-bin/man.cgi?section=3&topic=sysexits
|
||||
| Bash scripting:
|
||||
| http://tldp.org/LDP/abs/html/exitcodes.html
|
||||
|
|
||||
*/
|
||||
| --------------------------------------------------------------------------
|
||||
| Exit Status Codes
|
||||
| --------------------------------------------------------------------------
|
||||
|
|
||||
| Used to indicate the conditions under which the script is exit()ing.
|
||||
| While there is no universal standard for error codes, there are some
|
||||
| broad conventions. Three such conventions are mentioned below, for
|
||||
| those who wish to make use of them. The CodeIgniter defaults were
|
||||
| chosen for the least overlap with these conventions, while still
|
||||
| leaving room for others to be defined in future versions and user
|
||||
| applications.
|
||||
|
|
||||
| The three main conventions used for determining exit status codes
|
||||
| are as follows:
|
||||
|
|
||||
| Standard C/C++ Library (stdlibc):
|
||||
| http://www.gnu.org/software/libc/manual/html_node/Exit-Status.html
|
||||
| (This link also contains other GNU-specific conventions)
|
||||
| BSD sysexits.h:
|
||||
| http://www.gsp.com/cgi-bin/man.cgi?section=3&topic=sysexits
|
||||
| Bash scripting:
|
||||
| http://tldp.org/LDP/abs/html/exitcodes.html
|
||||
|
|
||||
*/
|
||||
defined('EXIT_SUCCESS') || define('EXIT_SUCCESS', 0); // no errors
|
||||
defined('EXIT_ERROR') || define('EXIT_ERROR', 1); // generic error
|
||||
defined('EXIT_CONFIG') || define('EXIT_CONFIG', 3); // configuration error
|
||||
|
|
|
|||
|
|
@ -5,45 +5,155 @@ namespace Config;
|
|||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
/**
|
||||
* Class ContentSecurityPolicyConfig
|
||||
*
|
||||
* Stores the default settings for the ContentSecurityPolicy, if you
|
||||
* choose to use it. The values here will be read in and set as defaults
|
||||
* for the site. If needed, they can be overridden on a page-by-page basis.
|
||||
*
|
||||
* Suggested reference for explanations:
|
||||
* https://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
||||
*
|
||||
* @package Config
|
||||
* @see https://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
||||
*/
|
||||
class ContentSecurityPolicy extends BaseConfig
|
||||
{
|
||||
// broadbrush CSP management
|
||||
//-------------------------------------------------------------------------
|
||||
// Broadbrush CSP management
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
public $reportOnly = false; // default CSP report context
|
||||
public $reportURI = null; // URL to send violation reports to
|
||||
public $upgradeInsecureRequests = false; // toggle for forcing https
|
||||
/**
|
||||
* Default CSP report context
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $reportOnly = false;
|
||||
|
||||
// sources allowed; string or array of strings
|
||||
/**
|
||||
* Specifies a URL where a browser will send reports
|
||||
* when a content security policy is violated.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $reportURI = null;
|
||||
|
||||
/**
|
||||
* Instructs user agents to rewrite URL schemes, changing
|
||||
* HTTP to HTTPS. This directive is for websites with
|
||||
* large numbers of old URLs that need to be rewritten.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $upgradeInsecureRequests = false;
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Sources allowed
|
||||
// Note: once you set a policy to 'none', it cannot be further restricted
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
public $defaultSrc = null; // will default to self if not over-ridden
|
||||
/**
|
||||
* Will default to self if not overridden
|
||||
*
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
public $defaultSrc = null;
|
||||
|
||||
/**
|
||||
* Lists allowed scripts' URLs.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $scriptSrc = 'self';
|
||||
|
||||
/**
|
||||
* Lists allowed stylesheets' URLs.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $styleSrc = 'self';
|
||||
|
||||
/**
|
||||
* Defines the origins from which images can be loaded.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $imageSrc = 'self';
|
||||
public $baseURI = null; // will default to self if not over-ridden
|
||||
|
||||
/**
|
||||
* Restricts the URLs that can appear in a page's `<base>` element.
|
||||
*
|
||||
* Will default to self if not overridden
|
||||
*
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
public $baseURI = null;
|
||||
|
||||
/**
|
||||
* Lists the URLs for workers and embedded frame contents
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $childSrc = 'self';
|
||||
|
||||
/**
|
||||
* Limits the origins that you can connect to (via XHR,
|
||||
* WebSockets, and EventSource).
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $connectSrc = 'self';
|
||||
|
||||
/**
|
||||
* Specifies the origins that can serve web fonts.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $fontSrc = null;
|
||||
|
||||
/**
|
||||
* Lists valid endpoints for submission from `<form>` tags.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $formAction = 'self';
|
||||
|
||||
/**
|
||||
* Specifies the sources that can embed the current page.
|
||||
* This directive applies to `<frame>`, `<iframe>`, `<embed>`,
|
||||
* and `<applet>` tags. This directive can't be used in
|
||||
* `<meta>` tags and applies only to non-HTML resources.
|
||||
*
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
public $frameAncestors = null;
|
||||
|
||||
/**
|
||||
* Restricts the origins allowed to deliver video and audio.
|
||||
*
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
public $mediaSrc = null;
|
||||
|
||||
/**
|
||||
* Allows control over Flash and other plugins.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
public $objectSrc = 'self';
|
||||
|
||||
/**
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
public $manifestSrc = null;
|
||||
|
||||
// mime types allowed; string or array of strings
|
||||
/**
|
||||
* Limits the kinds of plugins a page may invoke.
|
||||
*
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
public $pluginTypes = null;
|
||||
|
||||
// list of actions allowed; string or array of strings
|
||||
/**
|
||||
* List of actions allowed.
|
||||
*
|
||||
* @var string|string[]|null
|
||||
*/
|
||||
public $sandbox = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Database\Config;
|
||||
|
||||
/**
|
||||
* Database Configuration
|
||||
*
|
||||
* @package Config
|
||||
*/
|
||||
|
||||
class Database extends \CodeIgniter\Database\Config
|
||||
class Database extends Config
|
||||
{
|
||||
/**
|
||||
* The directory that holds the Migrations
|
||||
|
|
@ -16,7 +15,7 @@ class Database extends \CodeIgniter\Database\Config
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
public $filesPath = APPPATH . 'Database/';
|
||||
public $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
|
||||
|
||||
/**
|
||||
* Lets you choose which connection group to
|
||||
|
|
@ -41,10 +40,8 @@ class Database extends \CodeIgniter\Database\Config
|
|||
'DBPrefix' => 'cp_',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => ENVIRONMENT !== 'production',
|
||||
'cacheOn' => false,
|
||||
'cacheDir' => '',
|
||||
'charset' => 'utf8',
|
||||
'DBCollat' => 'utf8_general_ci',
|
||||
'charset' => 'utf8mb4',
|
||||
'DBCollat' => 'utf8mb4_unicode_ci',
|
||||
'swapPre' => '',
|
||||
'encrypt' => false,
|
||||
'compress' => false,
|
||||
|
|
@ -69,8 +66,6 @@ class Database extends \CodeIgniter\Database\Config
|
|||
'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
|
||||
'pConnect' => false,
|
||||
'DBDebug' => ENVIRONMENT !== 'production',
|
||||
'cacheOn' => false,
|
||||
'cacheDir' => '',
|
||||
'charset' => 'utf8',
|
||||
'DBCollat' => 'utf8_general_ci',
|
||||
'swapPre' => '',
|
||||
|
|
@ -92,21 +87,6 @@ class Database extends \CodeIgniter\Database\Config
|
|||
// we don't overwrite live data on accident.
|
||||
if (ENVIRONMENT === 'testing') {
|
||||
$this->defaultGroup = 'tests';
|
||||
|
||||
// Under Travis-CI, we can set an ENV var named 'DB_GROUP'
|
||||
// so that we can test against multiple databases.
|
||||
if ($group = getenv('DB')) {
|
||||
if (is_file(TESTPATH . 'travis/Database.php')) {
|
||||
require TESTPATH . 'travis/Database.php';
|
||||
|
||||
if (
|
||||
!empty($dbconfig) &&
|
||||
array_key_exists($group, $dbconfig)
|
||||
) {
|
||||
$this->tests = $dbconfig[$group];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* DocTypes
|
||||
*
|
||||
* @package Config
|
||||
*/
|
||||
|
||||
class DocTypes
|
||||
{
|
||||
/**
|
||||
* List of valid document types.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $list = [
|
||||
'xhtml11' =>
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
|
||||
|
|
|
|||
|
|
@ -12,25 +12,56 @@ use CodeIgniter\Config\BaseConfig;
|
|||
*/
|
||||
class Encryption extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key Starter
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you use the Encryption class you must set an encryption key (seed).
|
||||
| You need to ensure it is long enough for the cipher and mode you plan to use.
|
||||
| See the user guide for more info.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Encryption Key Starter
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If you use the Encryption class you must set an encryption key (seed).
|
||||
* You need to ensure it is long enough for the cipher and mode you plan to use.
|
||||
* See the user guide for more info.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
|
||||
public $key = '';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption driver to use
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| One of the supported drivers, eg 'OpenSSL' or 'Sodium'.
|
||||
| The default driver, if you don't specify one, is 'OpenSSL'.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Encryption Driver to Use
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* One of the supported encryption drivers.
|
||||
*
|
||||
* Available drivers:
|
||||
* - OpenSSL
|
||||
* - Sodium
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $driver = 'OpenSSL';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* SodiumHandler's Padding Length in Bytes
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This is the number of bytes that will be padded to the plaintext message
|
||||
* before it is encrypted. This value should be greater than zero.
|
||||
*
|
||||
* See the user guide for more information on padding.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $blockSize = 16;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Encryption digest
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* HMAC digest to use, e.g. 'SHA512' or 'SHA256'. Default value is 'SHA512'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $digest = 'SHA512';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Config;
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\Exceptions\FrameworkException;
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
|
|
@ -23,11 +24,15 @@ use CodeIgniter\Events\Events;
|
|||
|
||||
Events::on('pre_system', function () {
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
while (\ob_get_level() > 0) {
|
||||
\ob_end_flush();
|
||||
if (ini_get('zlib.output_compression')) {
|
||||
throw FrameworkException::forEnabledZlibOutputCompression();
|
||||
}
|
||||
|
||||
\ob_start(function ($buffer) {
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_flush();
|
||||
}
|
||||
|
||||
ob_start(function ($buffer) {
|
||||
return $buffer;
|
||||
});
|
||||
}
|
||||
|
|
@ -38,11 +43,98 @@ Events::on('pre_system', function () {
|
|||
* --------------------------------------------------------------------
|
||||
* If you delete, they will no longer be collected.
|
||||
*/
|
||||
if (ENVIRONMENT !== 'production') {
|
||||
if (CI_DEBUG) {
|
||||
Events::on(
|
||||
'DBQuery',
|
||||
'CodeIgniter\Debug\Toolbar\Collectors\Database::collect'
|
||||
'CodeIgniter\Debug\Toolbar\Collectors\Database::collect',
|
||||
);
|
||||
Services::toolbar()->respond();
|
||||
}
|
||||
});
|
||||
|
||||
Events::on('login', function ($user) {
|
||||
helper('auth');
|
||||
|
||||
// set interact_as_actor_id value
|
||||
$userPodcasts = $user->podcasts;
|
||||
if ($userPodcasts = $user->podcasts) {
|
||||
set_interact_as_actor($userPodcasts[0]->id);
|
||||
}
|
||||
});
|
||||
|
||||
Events::on('logout', function ($user) {
|
||||
helper('auth');
|
||||
|
||||
// remove user's interact_as_actor session
|
||||
remove_interact_as_actor($user->id);
|
||||
});
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* ActivityPub events
|
||||
* --------------------------------------------------------------------
|
||||
* Update episode metadata counts
|
||||
*/
|
||||
Events::on('on_note_add', function ($note) {
|
||||
if ($note->episode_id) {
|
||||
model('EpisodeModel')
|
||||
->where('id', $note->episode_id)
|
||||
->increment('notes_total');
|
||||
}
|
||||
});
|
||||
|
||||
Events::on('on_note_remove', function ($note) {
|
||||
if ($note->episode_id) {
|
||||
model('EpisodeModel')
|
||||
->where('id', $note->episode_id)
|
||||
->decrement('notes_total', 1 + $note->reblogs_count);
|
||||
|
||||
model('EpisodeModel')
|
||||
->where('id', $note->episode_id)
|
||||
->decrement('reblogs_total', $note->reblogs_count);
|
||||
|
||||
model('EpisodeModel')
|
||||
->where('id', $note->episode_id)
|
||||
->decrement('favourites_total', $note->favourites_count);
|
||||
}
|
||||
});
|
||||
|
||||
Events::on('on_note_reblog', function ($actor, $note) {
|
||||
if ($episodeId = $note->episode_id) {
|
||||
model('EpisodeModel')
|
||||
->where('id', $episodeId)
|
||||
->increment('reblogs_total');
|
||||
|
||||
model('EpisodeModel')
|
||||
->where('id', $episodeId)
|
||||
->increment('notes_total');
|
||||
}
|
||||
});
|
||||
|
||||
Events::on('on_note_undo_reblog', function ($reblogNote) {
|
||||
if ($episodeId = $reblogNote->reblog_of_note->episode_id) {
|
||||
model('EpisodeModel')
|
||||
->where('id', $episodeId)
|
||||
->decrement('reblogs_total');
|
||||
|
||||
model('EpisodeModel')
|
||||
->where('id', $episodeId)
|
||||
->decrement('notes_total');
|
||||
}
|
||||
});
|
||||
|
||||
Events::on('on_note_favourite', function ($actor, $note) {
|
||||
if ($note->episode_id) {
|
||||
model('EpisodeModel')
|
||||
->where('id', $note->episode_id)
|
||||
->increment('favourites_total');
|
||||
}
|
||||
});
|
||||
|
||||
Events::on('on_note_undo_favourite', function ($actor, $note) {
|
||||
if ($note->episode_id) {
|
||||
model('EpisodeModel')
|
||||
->where('id', $note->episode_id)
|
||||
->decrement('favourites_total');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,42 +2,47 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
/**
|
||||
* Setup how the exception handler works.
|
||||
*
|
||||
* @package Config
|
||||
*/
|
||||
|
||||
class Exceptions
|
||||
class Exceptions extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| LOG EXCEPTIONS?
|
||||
|--------------------------------------------------------------------------
|
||||
| If true, then exceptions will be logged
|
||||
| through Services::Log.
|
||||
|
|
||||
| Default: true
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* LOG EXCEPTIONS?
|
||||
* --------------------------------------------------------------------------
|
||||
* If true, then exceptions will be logged
|
||||
* through Services::Log.
|
||||
*
|
||||
* Default: true
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $log = true;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DO NOT LOG STATUS CODES
|
||||
|--------------------------------------------------------------------------
|
||||
| Any status codes here will NOT be logged if logging is turned on.
|
||||
| By default, only 404 (Page Not Found) exceptions are ignored.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* DO NOT LOG STATUS CODES
|
||||
* --------------------------------------------------------------------------
|
||||
* Any status codes here will NOT be logged if logging is turned on.
|
||||
* By default, only 404 (Page Not Found) exceptions are ignored.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $ignoreCodes = [404];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Views Path
|
||||
|--------------------------------------------------------------------------
|
||||
| This is the path to the directory that contains the 'cli' and 'html'
|
||||
| directories that hold the views used to generate errors.
|
||||
|
|
||||
| Default: APPPATH.'Views/errors'
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Error Views Path
|
||||
* --------------------------------------------------------------------------
|
||||
* This is the path to the directory that contains the 'cli' and 'html'
|
||||
* directories that hold the views used to generate errors.
|
||||
*
|
||||
* Default: APPPATH.'Views/errors'
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $errorViewPath = APPPATH . 'Views/errors';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,40 +3,65 @@
|
|||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Filters\CSRF;
|
||||
use CodeIgniter\Filters\DebugToolbar;
|
||||
use CodeIgniter\Filters\Honeypot;
|
||||
|
||||
class Filters extends BaseConfig
|
||||
{
|
||||
// Makes reading things below nicer,
|
||||
// and simpler to change out script that's used.
|
||||
/**
|
||||
* Configures aliases for Filter classes to
|
||||
* make reading things nicer and simpler.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $aliases = [
|
||||
'csrf' => \CodeIgniter\Filters\CSRF::class,
|
||||
'toolbar' => \CodeIgniter\Filters\DebugToolbar::class,
|
||||
'honeypot' => \CodeIgniter\Filters\Honeypot::class,
|
||||
'csrf' => CSRF::class,
|
||||
'toolbar' => DebugToolbar::class,
|
||||
'honeypot' => Honeypot::class,
|
||||
'login' => \Myth\Auth\Filters\LoginFilter::class,
|
||||
'role' => \Myth\Auth\Filters\RoleFilter::class,
|
||||
'permission' => \App\Filters\Permission::class,
|
||||
'permission' => \App\Filters\PermissionFilter::class,
|
||||
'activity-pub' => \ActivityPub\Filters\ActivityPubFilter::class,
|
||||
];
|
||||
|
||||
// Always applied before every request
|
||||
/**
|
||||
* List of filter aliases that are always
|
||||
* applied before and after every request.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $globals = [
|
||||
'before' => [
|
||||
//'honeypot'
|
||||
// 'honeypot',
|
||||
// 'csrf',
|
||||
],
|
||||
'after' => [
|
||||
'toolbar',
|
||||
//'honeypot'
|
||||
// 'honeypot',
|
||||
],
|
||||
];
|
||||
|
||||
// Works on all of a particular HTTP method
|
||||
// (GET, POST, etc) as BEFORE filters only
|
||||
// like: 'post' => ['CSRF', 'throttle'],
|
||||
/**
|
||||
* List of filter aliases that works on a
|
||||
* particular HTTP method (GET, POST, etc.).
|
||||
*
|
||||
* Example:
|
||||
* 'post' => ['csrf', 'throttle']
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $methods = [];
|
||||
|
||||
// List filter aliases and any before/after uri patterns
|
||||
// that they should run on, like:
|
||||
// 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']],
|
||||
/**
|
||||
* List of filter aliases that should run on any
|
||||
* before or after URI patterns.
|
||||
*
|
||||
* Example:
|
||||
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $filters = [];
|
||||
|
||||
public function __construct()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
class ForeignCharacters extends \CodeIgniter\Config\ForeignCharacters
|
||||
use CodeIgniter\Config\ForeignCharacters as BaseForeignCharacters;
|
||||
|
||||
class ForeignCharacters extends BaseForeignCharacters
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,43 +3,62 @@
|
|||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Format\FormatterInterface;
|
||||
|
||||
class Format extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available Response Formats
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you perform content negotiation with the request, these are the
|
||||
| available formats that your application supports. This is currently
|
||||
| only used with the API\ResponseTrait. A valid Formatter must exist
|
||||
| for the specified format.
|
||||
|
|
||||
| These formats are only checked when the data passed to the respond()
|
||||
| method is an array.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Available Response Formats
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* When you perform content negotiation with the request, these are the
|
||||
* available formats that your application supports. This is currently
|
||||
* only used with the API\ResponseTrait. A valid Formatter must exist
|
||||
* for the specified format.
|
||||
*
|
||||
* These formats are only checked when the data passed to the respond()
|
||||
* method is an array.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $supportedResponseFormats = [
|
||||
'application/json',
|
||||
'application/xml', // machine-readable XML
|
||||
'text/xml', // human-readable XML
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Formatters
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Lists the class to use to format responses with of a particular type.
|
||||
| For each mime type, list the class that should be used. Formatters
|
||||
| can be retrieved through the getFormatter() method.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Formatters
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Lists the class to use to format responses with of a particular type.
|
||||
* For each mime type, list the class that should be used. Formatters
|
||||
* can be retrieved through the getFormatter() method.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $formatters = [
|
||||
'application/json' => \CodeIgniter\Format\JSONFormatter::class,
|
||||
'application/xml' => \CodeIgniter\Format\XMLFormatter::class,
|
||||
'text/xml' => \CodeIgniter\Format\XMLFormatter::class,
|
||||
'application/json' => 'CodeIgniter\Format\JSONFormatter',
|
||||
'application/xml' => 'CodeIgniter\Format\XMLFormatter',
|
||||
'text/xml' => 'CodeIgniter\Format\XMLFormatter',
|
||||
];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Formatters Options
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Additional Options to adjust default formatters behaviour.
|
||||
* For each mime type, list the additional options that should be used.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
public $formatterOptions = [
|
||||
'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
'application/xml' => 0,
|
||||
'text/xml' => 0,
|
||||
];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
|
@ -49,26 +68,12 @@ class Format extends BaseConfig
|
|||
*
|
||||
* @param string $mime
|
||||
*
|
||||
* @return \CodeIgniter\Format\FormatterInterface
|
||||
* @return FormatterInterface
|
||||
*
|
||||
* @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead.
|
||||
*/
|
||||
public function getFormatter(string $mime)
|
||||
{
|
||||
if (!array_key_exists($mime, $this->formatters)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'No Formatter defined for mime type: ' . $mime
|
||||
);
|
||||
}
|
||||
|
||||
$class = $this->formatters[$mime];
|
||||
|
||||
if (!class_exists($class)) {
|
||||
throw new \BadMethodCallException(
|
||||
$class . ' is not a valid Formatter.'
|
||||
);
|
||||
}
|
||||
|
||||
return new $class();
|
||||
return Services::format()->getFormatter($mime);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
}
|
||||
|
|
|
|||
44
app/Config/Generators.php
Normal file
44
app/Config/Generators.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class Generators extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Generator Commands' Views
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This array defines the mapping of generator commands to the view files
|
||||
* they are using. If you need to customize them for your own, copy these
|
||||
* view files in your own folder and indicate the location here.
|
||||
*
|
||||
* You will notice that the views have special placeholders enclosed in
|
||||
* curly braces `{...}`. These placeholders are used internally by the
|
||||
* generator commands in processing replacements, thus you are warned
|
||||
* not to delete them or modify the names. If you will do so, you may
|
||||
* end up disrupting the scaffolding process and throw errors.
|
||||
*
|
||||
* YOU HAVE BEEN WARNED!
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $views = [
|
||||
'make:command' =>
|
||||
'CodeIgniter\Commands\Generators\Views\command.tpl.php',
|
||||
'make:controller' =>
|
||||
'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
|
||||
'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
|
||||
'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
|
||||
'make:migration' =>
|
||||
'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
|
||||
'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
|
||||
'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
|
||||
'make:validation' =>
|
||||
'CodeIgniter\Commands\Generators\Views\validation.tpl.php',
|
||||
'session:migration' =>
|
||||
'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
|
||||
];
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ class Honeypot extends BaseConfig
|
|||
* @var boolean
|
||||
*/
|
||||
public $hidden = true;
|
||||
|
||||
/**
|
||||
* Honeypot Label Content
|
||||
*
|
||||
|
|
@ -32,4 +33,11 @@ class Honeypot extends BaseConfig
|
|||
* @var string
|
||||
*/
|
||||
public $template = '<label>{label}</label><input type="text" name="{name}" value=""/>';
|
||||
|
||||
/**
|
||||
* Honeypot container
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $container = '<div style="display:none">{template}</div>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Images\Handlers\GDHandler;
|
||||
use CodeIgniter\Images\Handlers\ImageMagickHandler;
|
||||
|
||||
class Images extends BaseConfig
|
||||
{
|
||||
|
|
@ -24,20 +26,20 @@ class Images extends BaseConfig
|
|||
/**
|
||||
* The available handler classes.
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $handlers = [
|
||||
'gd' => \CodeIgniter\Images\Handlers\GDHandler::class,
|
||||
'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class,
|
||||
'gd' => GDHandler::class,
|
||||
'imagick' => ImageMagickHandler::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Uploaded images resizing sizes (in px)
|
||||
* --------------------------------------------------------------------------
|
||||
* The sizes listed below determine the resizing of images when uploaded.
|
||||
* All uploaded images are of 1:1 ratio (width and height are the same).
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Uploaded images resizing sizes (in px)
|
||||
|--------------------------------------------------------------------------
|
||||
| The sizes listed below determine the resizing of images when uploaded.
|
||||
| All uploaded images are of 1:1 ratio (width and height are the same).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
|
|
@ -68,12 +70,12 @@ class Images extends BaseConfig
|
|||
*/
|
||||
public $id3Size = 500;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Uploaded images naming extensions
|
||||
* --------------------------------------------------------------------------
|
||||
* The properties listed below set the name extensions for the resized images
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Uploaded images naming extensions
|
||||
|--------------------------------------------------------------------------
|
||||
| The properties listed below set the name extensions for the resized images
|
||||
*/
|
||||
|
||||
/**
|
||||
* @var string
|
||||
|
|
|
|||
|
|
@ -5,26 +5,23 @@ namespace Config;
|
|||
use CodeIgniter\Config\BaseConfig;
|
||||
use Kint\Renderer\Renderer;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Kint
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options
|
||||
* that you can set to customize how Kint works for you.
|
||||
*
|
||||
* @see https://kint-php.github.io/kint/ for details on these settings.
|
||||
*/
|
||||
class Kint extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Kint
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| We use Kint's RichRenderer and CLIRenderer. This area contains options
|
||||
| that you can set to customize how Kint works for you.
|
||||
|
|
||||
| For details on these settings, see Kint's docs:
|
||||
| https://kint-php.github.io/kint/
|
||||
|
|
||||
*/
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Global Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public $plugins = null;
|
||||
|
||||
|
|
@ -35,10 +32,10 @@ class Kint extends BaseConfig
|
|||
public $expanded = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| RichRenderer Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| RichRenderer Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
public $richTheme = 'aante-light.css';
|
||||
|
||||
public $richFolder = false;
|
||||
|
|
@ -50,10 +47,10 @@ class Kint extends BaseConfig
|
|||
public $richTabPlugins = null;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CLI Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| CLI Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
public $cliColors = true;
|
||||
|
||||
public $cliForceUTF8 = false;
|
||||
|
|
|
|||
|
|
@ -6,76 +6,82 @@ use CodeIgniter\Config\BaseConfig;
|
|||
|
||||
class Logger extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Logging Threshold
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can enable error logging by setting a threshold over zero. The
|
||||
| threshold determines what gets logged. Any values below or equal to the
|
||||
| threshold will be logged. Threshold options are:
|
||||
|
|
||||
| 0 = Disables logging, Error logging TURNED OFF
|
||||
| 1 = Emergency Messages - System is unusable
|
||||
| 2 = Alert Messages - Action Must Be Taken Immediately
|
||||
| 3 = Critical Messages - Application component unavailable, unexpected exception.
|
||||
| 4 = Runtime Errors - Don't need immediate action, but should be monitored.
|
||||
| 5 = Warnings - Exceptional occurrences that are not errors.
|
||||
| 6 = Notices - Normal but significant events.
|
||||
| 7 = Info - Interesting events, like user logging in, etc.
|
||||
| 8 = Debug - Detailed debug information.
|
||||
| 9 = All Messages
|
||||
|
|
||||
| You can also pass an array with threshold levels to show individual error types
|
||||
|
|
||||
| array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages
|
||||
|
|
||||
| For a live site you'll usually enable Critical or higher (3) to be logged otherwise
|
||||
| your log files will fill up very fast.
|
||||
|
|
||||
*/
|
||||
public $threshold = 3;
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Error Logging Threshold
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* You can enable error logging by setting a threshold over zero. The
|
||||
* threshold determines what gets logged. Any values below or equal to the
|
||||
* threshold will be logged.
|
||||
*
|
||||
* Threshold options are:
|
||||
*
|
||||
* - 0 = Disables logging, Error logging TURNED OFF
|
||||
* - 1 = Emergency Messages - System is unusable
|
||||
* - 2 = Alert Messages - Action Must Be Taken Immediately
|
||||
* - 3 = Critical Messages - Application component unavailable, unexpected exception.
|
||||
* - 4 = Runtime Errors - Don't need immediate action, but should be monitored.
|
||||
* - 5 = Warnings - Exceptional occurrences that are not errors.
|
||||
* - 6 = Notices - Normal but significant events.
|
||||
* - 7 = Info - Interesting events, like user logging in, etc.
|
||||
* - 8 = Debug - Detailed debug information.
|
||||
* - 9 = All Messages
|
||||
*
|
||||
* You can also pass an array with threshold levels to show individual error types
|
||||
*
|
||||
* array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages
|
||||
*
|
||||
* For a live site you'll usually enable Critical or higher (3) to be logged otherwise
|
||||
* your log files will fill up very fast.
|
||||
*
|
||||
* @var integer|array
|
||||
*/
|
||||
public $threshold = 4;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Date Format for Logs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Each item that is logged has an associated date. You can use PHP date
|
||||
| codes to set your own date formatting
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Date Format for Logs
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Each item that is logged has an associated date. You can use PHP date
|
||||
* codes to set your own date formatting
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Handlers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The logging system supports multiple actions to be taken when something
|
||||
| is logged. This is done by allowing for multiple Handlers, special classes
|
||||
| designed to write the log to their chosen destinations, whether that is
|
||||
| a file on the getServer, a cloud-based service, or even taking actions such
|
||||
| as emailing the dev team.
|
||||
|
|
||||
| Each handler is defined by the class name used for that handler, and it
|
||||
| MUST implement the CodeIgniter\Log\Handlers\HandlerInterface interface.
|
||||
|
|
||||
| The value of each key is an array of configuration items that are sent
|
||||
| to the constructor of each handler. The only required configuration item
|
||||
| is the 'handles' element, which must be an array of integer log levels.
|
||||
| This is most easily handled by using the constants defined in the
|
||||
| Psr\Log\LogLevel class.
|
||||
|
|
||||
| Handlers are executed in the order defined in this array, starting with
|
||||
| the handler on top and continuing down.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Log Handlers
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The logging system supports multiple actions to be taken when something
|
||||
* is logged. This is done by allowing for multiple Handlers, special classes
|
||||
* designed to write the log to their chosen destinations, whether that is
|
||||
* a file on the getServer, a cloud-based service, or even taking actions such
|
||||
* as emailing the dev team.
|
||||
*
|
||||
* Each handler is defined by the class name used for that handler, and it
|
||||
* MUST implement the `CodeIgniter\Log\Handlers\HandlerInterface` interface.
|
||||
*
|
||||
* The value of each key is an array of configuration items that are sent
|
||||
* to the constructor of each handler. The only required configuration item
|
||||
* is the 'handles' element, which must be an array of integer log levels.
|
||||
* This is most easily handled by using the constants defined in the
|
||||
* `Psr\Log\LogLevel` class.
|
||||
*
|
||||
* Handlers are executed in the order defined in this array, starting with
|
||||
* the handler on top and continuing down.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $handlers = [
|
||||
//--------------------------------------------------------------------
|
||||
// File Handler
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* File Handler
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
'CodeIgniter\Log\Handlers\FileHandler' => [
|
||||
/*
|
||||
* The log levels that this handler will handle.
|
||||
|
|
|
|||
|
|
@ -6,46 +6,50 @@ use CodeIgniter\Config\BaseConfig;
|
|||
|
||||
class Migrations extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable/Disable Migrations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Migrations are enabled by default for security reasons.
|
||||
| You should enable migrations whenever you intend to do a schema migration
|
||||
| and disable it back when you're done.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Enable/Disable Migrations
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Migrations are enabled by default.
|
||||
*
|
||||
* You should enable migrations whenever you intend to do a schema migration
|
||||
* and disable it back when you're done.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $enabled = true;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migrations table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the name of the table that will store the current migrations state.
|
||||
| When migrations runs it will store in a database table which migration
|
||||
| level the system is at. It then compares the migration level in this
|
||||
| table to the $config['migration_version'] if they are not the same it
|
||||
| will migrate up. This must be set.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Migrations Table
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This is the name of the table that will store the current migrations state.
|
||||
* When migrations runs it will store in a database table which migration
|
||||
* level the system is at. It then compares the migration level in this
|
||||
* table to the $config['migration_version'] if they are not the same it
|
||||
* will migrate up. This must be set.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $table = 'migrations';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Timestamp Format
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the format that will be used when creating new migrations
|
||||
| using the cli command:
|
||||
| > php spark migrate:create
|
||||
|
|
||||
| Typical formats:
|
||||
| YmdHis_
|
||||
| Y-m-d-His_
|
||||
| Y_m_d_His_
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Timestamp Format
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This is the format that will be used when creating new migrations
|
||||
* using the CLI command:
|
||||
* > php spark migrate:create
|
||||
*
|
||||
* Typical formats:
|
||||
* - YmdHis_
|
||||
* - Y-m-d-His_
|
||||
* - Y_m_d_His_
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $timestampFormat = 'Y-m-d-His_';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,21 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------
|
||||
| MIME TYPES
|
||||
| -------------------------------------------------------------------
|
||||
| This file contains an array of mime types. It is used by the
|
||||
| Upload class to help identify allowed file types.
|
||||
|
|
||||
| When more than one variation for an extension exist (like jpg, jpeg, etc)
|
||||
| the most common one should be first in the array to aid the guess*
|
||||
| methods. The same applies when more than one mime-type exists for a
|
||||
| single extension.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* Mimes
|
||||
*
|
||||
* This file contains an array of mime types. It is used by the
|
||||
* Upload class to help identify allowed file types.
|
||||
*
|
||||
* When more than one variation for an extension exist (like jpg, jpeg, etc)
|
||||
* the most common one should be first in the array to aid the guess*
|
||||
* methods. The same applies when more than one mime-type exists for a
|
||||
* single extension.
|
||||
*
|
||||
* When working with mime types, please make sure you have the ´fileinfo´
|
||||
* extension enabled to reliably detect the media types.
|
||||
*/
|
||||
|
||||
class Mimes
|
||||
{
|
||||
/**
|
||||
|
|
@ -34,7 +36,6 @@ class Mimes
|
|||
'text/csv',
|
||||
'text/x-comma-separated-values',
|
||||
'text/comma-separated-values',
|
||||
'application/octet-stream',
|
||||
'application/vnd.ms-excel',
|
||||
'application/x-csv',
|
||||
'text/x-csv',
|
||||
|
|
@ -64,7 +65,6 @@ class Mimes
|
|||
'application/pdf',
|
||||
'application/force-download',
|
||||
'application/x-download',
|
||||
'binary/octet-stream',
|
||||
],
|
||||
'ai' => ['application/pdf', 'application/postscript'],
|
||||
'eps' => 'application/postscript',
|
||||
|
|
@ -134,6 +134,7 @@ class Mimes
|
|||
'multipart/x-zip',
|
||||
],
|
||||
'rar' => [
|
||||
'application/vnd.rar',
|
||||
'application/x-rar',
|
||||
'application/rar',
|
||||
'application/x-rar-compressed',
|
||||
|
|
@ -305,15 +306,18 @@ class Mimes
|
|||
'application/x-jar',
|
||||
'application/x-compressed',
|
||||
],
|
||||
'svg' => ['image/svg+xml', 'application/xml', 'text/xml'],
|
||||
'svg' => ['image/svg+xml', 'image/svg', 'application/xml', 'text/xml'],
|
||||
'vcf' => 'text/x-vcard',
|
||||
'srt' => ['text/srt', 'text/plain', 'application/octet-stream'],
|
||||
'vtt' => ['text/vtt', 'text/plain'],
|
||||
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
|
||||
'stl' => [
|
||||
'application/sla',
|
||||
'application/vnd.ms-pki.stl',
|
||||
'application/x-navistyle',
|
||||
],
|
||||
];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Attempts to determine the best mime type for the given file extension.
|
||||
*
|
||||
|
|
@ -334,41 +338,47 @@ class Mimes
|
|||
: static::$mimes[$extension];
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Attempts to determine the best file extension for a given mime type.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $proposed_extension - default extension (in case there is more than one with the same mime type)
|
||||
* @param string $type
|
||||
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
|
||||
*
|
||||
* @return string|null The extension determined, or null if unable to match.
|
||||
*/
|
||||
public static function guessExtensionFromType(
|
||||
string $type,
|
||||
?string $proposed_extension = null
|
||||
string $proposedExtension = null
|
||||
) {
|
||||
$type = trim(strtolower($type), '. ');
|
||||
|
||||
$proposed_extension = trim(strtolower($proposed_extension));
|
||||
$proposedExtension = trim(strtolower($proposedExtension));
|
||||
|
||||
if (
|
||||
!is_null($proposed_extension) &&
|
||||
array_key_exists($proposed_extension, static::$mimes) &&
|
||||
in_array(
|
||||
$type,
|
||||
is_string(static::$mimes[$proposed_extension])
|
||||
? [static::$mimes[$proposed_extension]]
|
||||
: static::$mimes[$proposed_extension]
|
||||
)
|
||||
) {
|
||||
return $proposed_extension;
|
||||
if ($proposedExtension !== '') {
|
||||
if (
|
||||
array_key_exists($proposedExtension, static::$mimes) &&
|
||||
in_array(
|
||||
$type,
|
||||
is_string(static::$mimes[$proposedExtension])
|
||||
? [static::$mimes[$proposedExtension]]
|
||||
: static::$mimes[$proposedExtension],
|
||||
true,
|
||||
)
|
||||
) {
|
||||
// The detected mime type matches with the proposed extension.
|
||||
return $proposedExtension;
|
||||
}
|
||||
|
||||
// An extension was proposed, but the media type does not match the mime type list.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reverse check the mime type list if no extension was proposed.
|
||||
// This search is order sensitive!
|
||||
foreach (static::$mimes as $ext => $types) {
|
||||
if (
|
||||
(is_string($types) && $types === $type) ||
|
||||
(is_array($types) && in_array($type, $types))
|
||||
(is_array($types) && in_array($type, $types, true))
|
||||
) {
|
||||
return $ext;
|
||||
}
|
||||
|
|
@ -376,6 +386,4 @@ class Mimes
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,62 +2,46 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
// Cannot extend BaseConfig or looping resources occurs.
|
||||
class Modules
|
||||
use CodeIgniter\Modules\Modules as BaseModules;
|
||||
|
||||
class Modules extends BaseModules
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auto-Discovery Enabled?
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If true, then auto-discovery will happen across all elements listed in
|
||||
| $activeExplorers below. If false, no auto-discovery will happen at all,
|
||||
| giving a slight performance boost.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Enable Auto-Discovery?
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If true, then auto-discovery will happen across all elements listed in
|
||||
* $activeExplorers below. If false, no auto-discovery will happen at all,
|
||||
* giving a slight performance boost.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $enabled = true;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auto-Discovery Within Composer Packages Enabled?
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If true, then auto-discovery will happen across all namespaces loaded
|
||||
| by Composer, as well as the namespaces configured locally.
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Enable Auto-Discovery Within Composer Packages?
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If true, then auto-discovery will happen across all namespaces loaded
|
||||
* by Composer, as well as the namespaces configured locally.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $discoverInComposer = true;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auto-discover Rules
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Lists the aliases of all discovery classes that will be active
|
||||
| and used during the current application request. If it is not
|
||||
| listed here, only the base application elements will be used.
|
||||
*/
|
||||
public $activeExplorers = ['events', 'registrars', 'routes', 'services'];
|
||||
|
||||
/**
|
||||
* Should the application auto-discover the requested resources.
|
||||
* --------------------------------------------------------------------------
|
||||
* Auto-Discovery Rules
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Valid values are:
|
||||
* - events
|
||||
* - registrars
|
||||
* - routes
|
||||
* - services
|
||||
* Aliases list of all discovery classes that will be active and used during
|
||||
* the current application request.
|
||||
*
|
||||
* @param string $alias
|
||||
* If it is not listed, only the base application elements will be used.
|
||||
*
|
||||
* @return boolean
|
||||
* @var string[]
|
||||
*/
|
||||
public function shouldDiscover(string $alias)
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$alias = strtolower($alias);
|
||||
|
||||
return in_array($alias, $this->activeExplorers);
|
||||
}
|
||||
public $aliases = ['events', 'filters', 'registrars', 'routes', 'services'];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,32 +6,34 @@ use CodeIgniter\Config\BaseConfig;
|
|||
|
||||
class Pager extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Templates
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Pagination links are rendered out using views to configure their
|
||||
| appearance. This array contains aliases and the view names to
|
||||
| use when rendering the links.
|
||||
|
|
||||
| Within each view, the Pager object will be available as $pager,
|
||||
| and the desired group as $pagerGroup;
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Templates
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Pagination links are rendered out using views to configure their
|
||||
* appearance. This array contains aliases and the view names to
|
||||
* use when rendering the links.
|
||||
*
|
||||
* Within each view, the Pager object will be available as $pager,
|
||||
* and the desired group as $pagerGroup;
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $templates = [
|
||||
'default_full' => 'App\Views\pager\default_full',
|
||||
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
|
||||
'default_head' => 'CodeIgniter\Pager\Views\default_head',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Items Per Page
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default number of results shown in a single page.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Items Per Page
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The default number of results shown in a single page.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $perPage = 20;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
namespace Config;
|
||||
|
||||
/**
|
||||
* Paths
|
||||
*
|
||||
* Holds the paths that are used by the system to
|
||||
* locate the main directories, app, system, etc.
|
||||
* Modifying these allows you to re-structure your application,
|
||||
*
|
||||
* Modifying these allows you to restructure your application,
|
||||
* share a system folder between multiple applications, and more.
|
||||
*
|
||||
* All paths are relative to the project's root folder.
|
||||
|
|
@ -13,34 +16,35 @@ namespace Config;
|
|||
|
||||
class Paths
|
||||
{
|
||||
/*
|
||||
*---------------------------------------------------------------
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* SYSTEM FOLDER NAME
|
||||
*---------------------------------------------------------------
|
||||
* ---------------------------------------------------------------
|
||||
*
|
||||
* This variable must contain the name of your "system" folder.
|
||||
* Include the path if the folder is not in the same directory
|
||||
* as this file.
|
||||
* This must contain the name of your "system" folder. Include
|
||||
* the path if the folder is not in the same directory as this file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $systemDirectory =
|
||||
__DIR__ . '/../../vendor/codeigniter4/codeigniter4/system';
|
||||
|
||||
/*
|
||||
*---------------------------------------------------------------
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* APPLICATION FOLDER NAME
|
||||
*---------------------------------------------------------------
|
||||
* ---------------------------------------------------------------
|
||||
*
|
||||
* If you want this front controller to use a different "app"
|
||||
* folder than the default one you can set its name here. The folder
|
||||
* can also be renamed or relocated anywhere on your getServer. If
|
||||
* you do, use a full getServer path. For more info please see the user guide:
|
||||
* http://codeigniter.com/user_guide/general/managing_apps.html
|
||||
* you do, use a full getServer path.
|
||||
*
|
||||
* NO TRAILING SLASH!
|
||||
* @see http://codeigniter.com/user_guide/general/managing_apps.html
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $appDirectory = __DIR__ . '/..';
|
||||
|
||||
/*
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* WRITABLE DIRECTORY NAME
|
||||
* ---------------------------------------------------------------
|
||||
|
|
@ -50,23 +54,23 @@ class Paths
|
|||
* need write permission to a single place that can be tucked away
|
||||
* for maximum security, keeping it out of the app and/or
|
||||
* system directories.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $writableDirectory = __DIR__ . '/../../writable';
|
||||
|
||||
/*
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* TESTS DIRECTORY NAME
|
||||
* ---------------------------------------------------------------
|
||||
*
|
||||
* This variable must contain the name of your "tests" directory.
|
||||
* The writable directory allows you to group all directories that
|
||||
* need write permission to a single place that can be tucked away
|
||||
* for maximum security, keeping it out of the app and/or
|
||||
* system directories.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $testsDirectory = __DIR__ . '/../../tests';
|
||||
|
||||
/*
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* VIEW DIRECTORY NAME
|
||||
* ---------------------------------------------------------------
|
||||
|
|
@ -75,6 +79,8 @@ class Paths
|
|||
* contains the view files used by your application. By
|
||||
* default this is in `app/Views`. This value
|
||||
* is used when no value is provided to `Services::renderer()`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $viewDirectory = __DIR__ . '/../Views';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,10 +29,19 @@ $routes->setAutoRoute(false);
|
|||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}');
|
||||
$routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,32}');
|
||||
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}');
|
||||
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
|
||||
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
|
||||
$routes->addPlaceholder('noteAction', '\bfavourite|\breblog|\breply');
|
||||
$routes->addPlaceholder(
|
||||
'embeddablePlayerTheme',
|
||||
'\blight|\bdark|\blight-transparent|\bdark-transparent',
|
||||
);
|
||||
$routes->addPlaceholder(
|
||||
'uuid',
|
||||
'[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}',
|
||||
);
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
|
|
@ -62,7 +71,7 @@ $routes->group(config('App')->installGateway, function ($routes) {
|
|||
});
|
||||
|
||||
// Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
|
||||
$routes->add('audio/(:base64)/(:any)', 'Analytics::hit/$1/$2', [
|
||||
$routes->get('audio/(:base64)/(:any)', 'Analytics::hit/$1/$2', [
|
||||
'as' => 'analytics_hit',
|
||||
]);
|
||||
|
||||
|
|
@ -150,7 +159,7 @@ $routes->group(
|
|||
$routes->post('edit', 'Podcast::attemptEdit/$1', [
|
||||
'filter' => 'permission:podcast-edit',
|
||||
]);
|
||||
$routes->add('delete', 'Podcast::delete/$1', [
|
||||
$routes->get('delete', 'Podcast::delete/$1', [
|
||||
'as' => 'podcast-delete',
|
||||
'filter' => 'permission:podcasts-delete',
|
||||
]);
|
||||
|
|
@ -170,7 +179,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'podcast-person-remove',
|
||||
'filter' => 'permission:podcast-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -185,7 +194,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'podcast-analytics-webpages',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'locations',
|
||||
|
|
@ -193,7 +202,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'podcast-analytics-locations',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'unique-listeners',
|
||||
|
|
@ -201,7 +210,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'podcast-analytics-unique-listeners',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'listening-time',
|
||||
|
|
@ -209,7 +218,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'podcast-analytics-listening-time',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'time-periods',
|
||||
|
|
@ -217,7 +226,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'podcast-analytics-time-periods',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'players',
|
||||
|
|
@ -225,7 +234,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'podcast-analytics-players',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -235,7 +244,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'analytics-full-data',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'analytics-data/(:segment)/(:segment)',
|
||||
|
|
@ -243,7 +252,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'analytics-data',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'analytics-data/(:segment)/(:segment)/(:num)',
|
||||
|
|
@ -251,7 +260,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'analytics-filtered-data',
|
||||
'filter' => 'permission:podcasts-view,podcast-view',
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
// Podcast episodes
|
||||
|
|
@ -283,7 +292,50 @@ $routes->group(
|
|||
$routes->post('edit', 'Episode::attemptEdit/$1/$2', [
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
]);
|
||||
$routes->add('delete', 'Episode::delete/$1/$2', [
|
||||
$routes->get('publish', 'Episode::publish/$1/$2', [
|
||||
'as' => 'episode-publish',
|
||||
'filter' =>
|
||||
'permission:podcast-manage_publications',
|
||||
]);
|
||||
$routes->post(
|
||||
'publish',
|
||||
'Episode::attemptPublish/$1/$2',
|
||||
[
|
||||
'filter' =>
|
||||
'permission:podcast-manage_publications',
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'publish-edit',
|
||||
'Episode::publishEdit/$1/$2',
|
||||
[
|
||||
'as' => 'episode-publish_edit',
|
||||
'filter' =>
|
||||
'permission:podcast-manage_publications',
|
||||
],
|
||||
);
|
||||
$routes->post(
|
||||
'publish-edit',
|
||||
'Episode::attemptPublishEdit/$1/$2',
|
||||
[
|
||||
'filter' =>
|
||||
'permission:podcast-manage_publications',
|
||||
],
|
||||
);
|
||||
$routes->get('unpublish', 'Episode::unpublish/$1/$2', [
|
||||
'as' => 'episode-unpublish',
|
||||
'filter' =>
|
||||
'permission:podcast-manage_publications',
|
||||
]);
|
||||
$routes->post(
|
||||
'unpublish',
|
||||
'Episode::attemptUnpublish/$1/$2',
|
||||
[
|
||||
'filter' =>
|
||||
'permission:podcast-manage_publications',
|
||||
],
|
||||
);
|
||||
$routes->get('delete', 'Episode::delete/$1/$2', [
|
||||
'as' => 'episode-delete',
|
||||
'filter' => 'permission:podcast_episodes-delete',
|
||||
]);
|
||||
|
|
@ -293,7 +345,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'transcript-delete',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'chapters-delete',
|
||||
|
|
@ -301,7 +353,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'chapters-delete',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'soundbites',
|
||||
|
|
@ -309,22 +361,22 @@ $routes->group(
|
|||
[
|
||||
'as' => 'soundbites-edit',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->post(
|
||||
'soundbites',
|
||||
'Episode::soundbitesAttemptEdit/$1/$2',
|
||||
[
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->add(
|
||||
$routes->get(
|
||||
'soundbites/(:num)/delete',
|
||||
'Episode::soundbiteDelete/$1/$2/$3',
|
||||
[
|
||||
'as' => 'soundbite-delete',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'embeddable-player',
|
||||
|
|
@ -332,7 +384,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'embeddable-player-add',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
$routes->group('persons', function ($routes) {
|
||||
|
|
@ -346,7 +398,7 @@ $routes->group(
|
|||
[
|
||||
'filter' =>
|
||||
'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'(:num)/remove',
|
||||
|
|
@ -355,7 +407,7 @@ $routes->group(
|
|||
'as' => 'episode-person-remove',
|
||||
'filter' =>
|
||||
'permission:podcast_episodes-edit',
|
||||
]
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -394,9 +446,9 @@ $routes->group(
|
|||
[
|
||||
'filter' =>
|
||||
'permission:podcast-manage_contributors',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->add('remove', 'Contributor::remove/$1/$2', [
|
||||
$routes->get('remove', 'Contributor::remove/$1/$2', [
|
||||
'as' => 'contributor-remove',
|
||||
'filter' =>
|
||||
'permission:podcast-manage_contributors',
|
||||
|
|
@ -411,7 +463,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'platforms-podcasting',
|
||||
'filter' => 'permission:podcast-manage_platforms',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'social',
|
||||
|
|
@ -419,7 +471,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'platforms-social',
|
||||
'filter' => 'permission:podcast-manage_platforms',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'funding',
|
||||
|
|
@ -427,7 +479,7 @@ $routes->group(
|
|||
[
|
||||
'as' => 'platforms-funding',
|
||||
'filter' => 'permission:podcast-manage_platforms',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->post(
|
||||
'save/(:platformType)',
|
||||
|
|
@ -435,20 +487,35 @@ $routes->group(
|
|||
[
|
||||
'as' => 'platforms-save',
|
||||
'filter' => 'permission:podcast-manage_platforms',
|
||||
]
|
||||
],
|
||||
);
|
||||
$routes->add(
|
||||
$routes->get(
|
||||
'(:slug)/podcast-platform-remove',
|
||||
'PodcastPlatform::removePodcastPlatform/$1/$2',
|
||||
[
|
||||
'as' => 'podcast-platform-remove',
|
||||
'filter' => 'permission:podcast-manage_platforms',
|
||||
]
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Instance wide Fediverse config
|
||||
$routes->group('fediverse', function ($routes) {
|
||||
$routes->get('/', 'Fediverse::dashboard', [
|
||||
'as' => 'fediverse-dashboard',
|
||||
]);
|
||||
$routes->get('blocked-actors', 'Fediverse::blockedActors', [
|
||||
'as' => 'fediverse-blocked-actors',
|
||||
'filter' => 'permission:fediverse-block_actors',
|
||||
]);
|
||||
$routes->get('blocked-domains', 'Fediverse::blockedDomains', [
|
||||
'as' => 'fediverse-blocked-domains',
|
||||
'filter' => 'permission:fediverse-block_domains',
|
||||
]);
|
||||
});
|
||||
|
||||
// Pages
|
||||
$routes->group('pages', function ($routes) {
|
||||
$routes->get('/', 'Page::list', ['as' => 'page-list']);
|
||||
|
|
@ -470,7 +537,7 @@ $routes->group(
|
|||
'filter' => 'permission:pages-manage',
|
||||
]);
|
||||
|
||||
$routes->add('delete', 'Page::delete/$1', [
|
||||
$routes->get('delete', 'Page::delete/$1', [
|
||||
'as' => 'page-delete',
|
||||
'filter' => 'permission:pages-manage',
|
||||
]);
|
||||
|
|
@ -504,19 +571,19 @@ $routes->group(
|
|||
$routes->post('edit', 'User::attemptEdit/$1', [
|
||||
'filter' => 'permission:users-manage_authorizations',
|
||||
]);
|
||||
$routes->add('ban', 'User::ban/$1', [
|
||||
$routes->get('ban', 'User::ban/$1', [
|
||||
'as' => 'user-ban',
|
||||
'filter' => 'permission:users-manage_bans',
|
||||
]);
|
||||
$routes->add('unban', 'User::unBan/$1', [
|
||||
$routes->get('unban', 'User::unBan/$1', [
|
||||
'as' => 'user-unban',
|
||||
'filter' => 'permission:users-manage_bans',
|
||||
]);
|
||||
$routes->add('force-pass-reset', 'User::forcePassReset/$1', [
|
||||
$routes->get('force-pass-reset', 'User::forcePassReset/$1', [
|
||||
'as' => 'user-force_pass_reset',
|
||||
'filter' => 'permission:users-force_pass_reset',
|
||||
]);
|
||||
$routes->add('delete', 'User::delete/$1', [
|
||||
$routes->get('delete', 'User::delete/$1', [
|
||||
'as' => 'user-delete',
|
||||
'filter' => 'permission:users-delete',
|
||||
]);
|
||||
|
|
@ -533,7 +600,7 @@ $routes->group(
|
|||
]);
|
||||
$routes->post('change-password', 'MyAccount::attemptChange/$1');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
@ -570,35 +637,142 @@ $routes->group(config('App')->authGateway, function ($routes) {
|
|||
$routes->post('reset-password', 'Auth::attemptReset');
|
||||
});
|
||||
|
||||
// Public routes
|
||||
// Podcast's Public routes
|
||||
$routes->group('@(:podcastName)', function ($routes) {
|
||||
$routes->get('/', 'Podcast/$1', ['as' => 'podcast']);
|
||||
$routes->group('(:slug)', function ($routes) {
|
||||
$routes->get('/', 'Podcast::activity/$1', [
|
||||
'as' => 'podcast-activity',
|
||||
]);
|
||||
// override default ActivityPub Library's actor route
|
||||
$routes->get('/', 'Podcast::activity/$1', [
|
||||
'as' => 'actor',
|
||||
'alternate-content' => [
|
||||
'application/activity+json' => [
|
||||
'namespace' => 'ActivityPub\Controllers',
|
||||
'controller-method' => 'ActorController/$1',
|
||||
],
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
||||
'namespace' => 'ActivityPub\Controllers',
|
||||
'controller-method' => 'ActorController/$1',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$routes->get('episodes', 'Podcast::episodes/$1', [
|
||||
'as' => 'podcast-episodes',
|
||||
]);
|
||||
$routes->group('episodes/(:slug)', function ($routes) {
|
||||
$routes->get('/', 'Episode/$1/$2', [
|
||||
'as' => 'episode',
|
||||
]);
|
||||
$routes->get('oembed.json', 'Episode::oembedJSON/$1/$2', [
|
||||
'as' => 'episode-oembed-json',
|
||||
]);
|
||||
$routes->get('oembed.xml', 'Episode::oembedXML/$1/$2', [
|
||||
'as' => 'episode-oembed-xml',
|
||||
]);
|
||||
$routes->group('embeddable-player', function ($routes) {
|
||||
$routes->get('/', 'Episode::embeddablePlayer/$1/$2', [
|
||||
'as' => 'embeddable-player',
|
||||
]);
|
||||
$routes->get('(:slug)', 'Episode::embeddablePlayer/$1/$2/$3', [
|
||||
'as' => 'embeddable-player-theme',
|
||||
]);
|
||||
$routes->get(
|
||||
'(:embeddablePlayerTheme)',
|
||||
'Episode::embeddablePlayer/$1/$2/$3',
|
||||
[
|
||||
'as' => 'embeddable-player-theme',
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
$routes->head('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
|
||||
$routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
|
||||
});
|
||||
|
||||
// Other pages
|
||||
$routes->get('/credits', 'Page::credits', ['as' => 'credits']);
|
||||
$routes->get('/(:slug)', 'Page/$1', ['as' => 'page']);
|
||||
|
||||
// interacting as an actor
|
||||
$routes->post('interact-as-actor', 'Auth::attemptInteractAsActor', [
|
||||
'as' => 'interact-as-actor',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Overwriting ActivityPub routes file
|
||||
*/
|
||||
$routes->group('@(:podcastName)', function ($routes) {
|
||||
$routes->post('notes/new', 'Note::attemptCreate/$1', [
|
||||
'as' => 'note-attempt-create',
|
||||
'filter' => 'permission:podcast-manage_publications',
|
||||
]);
|
||||
// Note
|
||||
$routes->group('notes/(:uuid)', function ($routes) {
|
||||
$routes->get('/', 'Note/$1/$2', [
|
||||
'as' => 'note',
|
||||
'alternate-content' => [
|
||||
'application/activity+json' => [
|
||||
'namespace' => 'ActivityPub\Controllers',
|
||||
'controller-method' => 'NoteController/$2',
|
||||
],
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
||||
'namespace' => 'ActivityPub\Controllers',
|
||||
'controller-method' => 'NoteController/$2',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$routes->get('replies', 'Note/$1/$2', [
|
||||
'as' => 'note-replies',
|
||||
'alternate-content' => [
|
||||
'application/activity+json' => [
|
||||
'namespace' => 'ActivityPub\Controllers',
|
||||
'controller-method' => 'NoteController::replies/$2',
|
||||
],
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
|
||||
'namespace' => 'ActivityPub\Controllers',
|
||||
'controller-method' => 'NoteController::replies/$2',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Actions
|
||||
$routes->post('action', 'Note::attemptAction/$1/$2', [
|
||||
'as' => 'note-attempt-action',
|
||||
'filter' => 'permission:podcast-interact_as',
|
||||
]);
|
||||
|
||||
$routes->post('block-actor', 'Note::attemptBlockActor/$1/$2', [
|
||||
'as' => 'note-attempt-block-actor',
|
||||
'filter' => 'permission:fediverse-block_actors',
|
||||
]);
|
||||
$routes->post('block-domain', 'Note::attemptBlockDomain/$1/$2', [
|
||||
'as' => 'note-attempt-block-domain',
|
||||
'filter' => 'permission:fediverse-block_domains',
|
||||
]);
|
||||
$routes->post('delete', 'Note::attemptDelete/$1/$2', [
|
||||
'as' => 'note-attempt-delete',
|
||||
'filter' => 'permission:podcast-manage_publications',
|
||||
]);
|
||||
|
||||
$routes->get('remote/(:noteAction)', 'Note::remoteAction/$1/$2/$3', [
|
||||
'as' => 'note-remote-action',
|
||||
]);
|
||||
});
|
||||
|
||||
$routes->get('follow', 'Actor::follow/$1', [
|
||||
'as' => 'follow',
|
||||
]);
|
||||
$routes->get('outbox', 'Actor::outbox/$1', [
|
||||
'as' => 'outbox',
|
||||
'filter' => 'activity-pub:verify-activitystream',
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* Additional Routing
|
||||
* --------------------------------------------------------------------
|
||||
*
|
||||
* There will often be times that you need additional routing and you
|
||||
* need to it be able to override any defaults in this file. Environment
|
||||
* need it to be able to override any defaults in this file. Environment
|
||||
* based routes is one such time. require() additional route files here
|
||||
* to make that happen.
|
||||
*
|
||||
|
|
|
|||
92
app/Config/Security.php
Normal file
92
app/Config/Security.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class Security extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Token Name
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Token name for Cross Site Request Forgery protection cookie.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $tokenName = 'csrf_test_name';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Header Name
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Token name for Cross Site Request Forgery protection cookie.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $headerName = 'X-CSRF-TOKEN';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Cookie Name
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Cookie name for Cross Site Request Forgery protection cookie.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $cookieName = 'csrf_cookie_name';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Expires
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Expiration time for Cross Site Request Forgery protection cookie.
|
||||
*
|
||||
* Defaults to two hours (in seconds).
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $expires = 7200;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Regenerate
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Regenerate CSRF Token on every request.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $regenerate = true;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF Redirect
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Redirect to previous page with error on failure.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $redirect = true;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CSRF SameSite
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Setting for CSRF SameSite cookie token.
|
||||
*
|
||||
* Allowed values are: None - Lax - Strict - ''.
|
||||
*
|
||||
* Defaults to `Lax` as recommended in this link:
|
||||
* @see https://portswigger.net/web-security/csrf/samesite-cookies
|
||||
*
|
||||
* @var string 'Lax'|'None'|'Strict'
|
||||
*/
|
||||
public $samesite = 'Lax';
|
||||
}
|
||||
|
|
@ -2,17 +2,20 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\Services as CoreServices;
|
||||
use CodeIgniter\Config\BaseService;
|
||||
use CodeIgniter\Model;
|
||||
use App\Authorization\FlatAuthorization;
|
||||
use App\Authorization\PermissionModel;
|
||||
use App\Authorization\GroupModel;
|
||||
use App\Libraries\Breadcrumb;
|
||||
use App\Libraries\Negotiate;
|
||||
use App\Libraries\Router;
|
||||
use App\Models\UserModel;
|
||||
use CodeIgniter\HTTP\Request;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\Router\RouteCollectionInterface;
|
||||
use Myth\Auth\Models\LoginModel;
|
||||
|
||||
require_once SYSTEMPATH . 'Config/Services.php';
|
||||
|
||||
/**
|
||||
* Services Configuration file.
|
||||
*
|
||||
|
|
@ -26,8 +29,56 @@ require_once SYSTEMPATH . 'Config/Services.php';
|
|||
* method format you should use for your service methods. For more examples,
|
||||
* see the core Services file at system/Config/Services.php.
|
||||
*/
|
||||
class Services extends CoreServices
|
||||
class Services extends BaseService
|
||||
{
|
||||
/**
|
||||
* The Router class uses a RouteCollection's array of routes, and determines
|
||||
* the correct Controller and Method to execute.
|
||||
*
|
||||
* @param RouteCollectionInterface|null $routes
|
||||
* @param Request|null $request
|
||||
* @param boolean $getShared
|
||||
*
|
||||
* @return Router
|
||||
*/
|
||||
public static function router(
|
||||
RouteCollectionInterface $routes = null,
|
||||
Request $request = null,
|
||||
bool $getShared = true
|
||||
) {
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('router', $routes, $request);
|
||||
}
|
||||
|
||||
$routes = $routes ?? static::routes();
|
||||
$request = $request ?? static::request();
|
||||
|
||||
return new Router($routes, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Negotiate class provides the content negotiation features for
|
||||
* working the request to determine correct language, encoding, charset,
|
||||
* and more.
|
||||
*
|
||||
* @param RequestInterface|null $request
|
||||
* @param boolean $getShared
|
||||
*
|
||||
* @return Negotiate
|
||||
*/
|
||||
public static function negotiator(
|
||||
RequestInterface $request = null,
|
||||
bool $getShared = true
|
||||
) {
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('negotiator', $request);
|
||||
}
|
||||
|
||||
$request = $request ?? static::request();
|
||||
|
||||
return new Negotiate($request);
|
||||
}
|
||||
|
||||
public static function authentication(
|
||||
string $lib = 'local',
|
||||
Model $userModel = null,
|
||||
|
|
@ -39,7 +90,7 @@ class Services extends CoreServices
|
|||
'authentication',
|
||||
$lib,
|
||||
$userModel,
|
||||
$loginModel
|
||||
$loginModel,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +123,7 @@ class Services extends CoreServices
|
|||
'authorization',
|
||||
$groupModel,
|
||||
$permissionModel,
|
||||
$userModel
|
||||
$userModel,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,69 +3,85 @@
|
|||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Database;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Events;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Files;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Logs;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Routes;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Timers;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Views;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Debug Toolbar
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The Debug Toolbar provides a way to see information about the performance
|
||||
* and state of your application during that page display. By default it will
|
||||
* NOT be displayed under production environments, and will only display if
|
||||
* `CI_DEBUG` is true, since if it's not, there's not much to display anyway.
|
||||
*/
|
||||
class Toolbar extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Debug Toolbar
|
||||
|--------------------------------------------------------------------------
|
||||
| The Debug Toolbar provides a way to see information about the performance
|
||||
| and state of your application during that page display. By default it will
|
||||
| NOT be displayed under production environments, and will only display if
|
||||
| CI_DEBUG is true, since if it's not, there's not much to display anyway.
|
||||
|
|
||||
| toolbarMaxHistory = Number of history files, 0 for none or -1 for unlimited
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Toolbar Collectors
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* List of toolbar collectors that will be called when Debug Toolbar
|
||||
* fires up and collects data from.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $collectors = [
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Timers::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Database::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Logs::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Views::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Cache::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Files::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Routes::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Events::class,
|
||||
\Myth\Auth\Collectors\Auth::class,
|
||||
Timers::class,
|
||||
Database::class,
|
||||
Logs::class,
|
||||
Views::class,
|
||||
// Cache::class,
|
||||
Files::class,
|
||||
Routes::class,
|
||||
Events::class,
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Max History
|
||||
|--------------------------------------------------------------------------
|
||||
| The Toolbar allows you to view recent requests that have been made to
|
||||
| the application while the toolbar is active. This allows you to quickly
|
||||
| view and compare multiple requests.
|
||||
|
|
||||
| $maxHistory sets a limit on the number of past requests that are stored,
|
||||
| helping to conserve file space used to store them. You can set it to
|
||||
| 0 (zero) to not have any history stored, or -1 for unlimited history.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Max History
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* `$maxHistory` sets a limit on the number of past requests that are stored,
|
||||
* helping to conserve file space used to store them. You can set it to
|
||||
* 0 (zero) to not have any history stored, or -1 for unlimited history.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $maxHistory = 20;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Toolbar Views Path
|
||||
|--------------------------------------------------------------------------
|
||||
| The full path to the the views that are used by the toolbar.
|
||||
| MUST have a trailing slash.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Toolbar Views Path
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The full path to the the views that are used by the toolbar.
|
||||
* This MUST have a trailing slash.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $viewsPath = SYSTEMPATH . 'Debug/Toolbar/Views/';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Max Queries
|
||||
|--------------------------------------------------------------------------
|
||||
| If the Database Collector is enabled, it will log every query that the
|
||||
| the system generates so they can be displayed on the toolbar's timeline
|
||||
| and in the query log. This can lead to memory issues in some instances
|
||||
| with hundreds of queries.
|
||||
|
|
||||
| $maxQueries defines the maximum amount of queries that will be stored.
|
||||
|
|
||||
*/
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Max Queries
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If the Database Collector is enabled, it will log every query that the
|
||||
* the system generates so they can be displayed on the toolbar's timeline
|
||||
* and in the query log. This can lead to memory issues in some instances
|
||||
* with hundreds of queries.
|
||||
*
|
||||
* `$maxQueries` defines the maximum amount of queries that will be stored.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $maxQueries = 100;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* User Agents
|
||||
* -------------------------------------------------------------------
|
||||
*
|
||||
* This file contains four arrays of user agent data. It is used by the
|
||||
* User Agent Class to help identify browser, platform, robot, and
|
||||
* mobile device data. The array keys are used to identify the device
|
||||
* and the array values are used to set the actual name of the item.
|
||||
*/
|
||||
class UserAgents extends BaseConfig
|
||||
{
|
||||
/*
|
||||
| -------------------------------------------------------------------
|
||||
| USER AGENT TYPES
|
||||
| -------------------------------------------------------------------
|
||||
| This file contains four arrays of user agent data. It is used by the
|
||||
| User Agent Class to help identify browser, platform, robot, and
|
||||
| mobile device data. The array keys are used to identify the device
|
||||
| and the array values are used to set the actual name of the item.
|
||||
*/
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* OS Platforms
|
||||
* -------------------------------------------------------------------
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $platforms = [
|
||||
'windows nt 10.0' => 'Windows 10',
|
||||
'windows nt 6.3' => 'Windows 8.1',
|
||||
|
|
@ -60,12 +67,21 @@ class UserAgents extends BaseConfig
|
|||
'symbian' => 'Symbian OS',
|
||||
];
|
||||
|
||||
// The order of this array should NOT be changed. Many browsers return
|
||||
// multiple browser types so we want to identify the sub-type first.
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Browsers
|
||||
* -------------------------------------------------------------------
|
||||
*
|
||||
* The order of this array should NOT be changed. Many browsers return
|
||||
* multiple browser types so we want to identify the subtype first.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $browsers = [
|
||||
'OPR' => 'Opera',
|
||||
'Flock' => 'Flock',
|
||||
'Edge' => 'Spartan',
|
||||
'Edg' => 'Edge',
|
||||
'Chrome' => 'Chrome',
|
||||
// Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string
|
||||
'Opera.*?Version' => 'Opera',
|
||||
|
|
@ -95,6 +111,13 @@ class UserAgents extends BaseConfig
|
|||
'Vivaldi' => 'Vivaldi',
|
||||
];
|
||||
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Mobiles
|
||||
* -------------------------------------------------------------------
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $mobiles = [
|
||||
// legacy array, old values commented out
|
||||
'mobileexplorer' => 'Mobile Explorer',
|
||||
|
|
@ -195,7 +218,15 @@ class UserAgents extends BaseConfig
|
|||
'cellphone' => 'Generic Mobile',
|
||||
];
|
||||
|
||||
// There are hundreds of bots but these are the most common.
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Robots
|
||||
* -------------------------------------------------------------------
|
||||
*
|
||||
* There are hundred of bots but these are the most common.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $robots = [
|
||||
'googlebot' => 'Googlebot',
|
||||
'msnbot' => 'MSNBot',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
use App\Validation\FileRules as AppFileRules;
|
||||
use App\Validation\Rules as AppRules;
|
||||
use CodeIgniter\Validation\CreditCardRules;
|
||||
use CodeIgniter\Validation\FileRules;
|
||||
use CodeIgniter\Validation\FormatRules;
|
||||
use CodeIgniter\Validation\Rules;
|
||||
use Myth\Auth\Authentication\Passwords\ValidationRules as PasswordRules;
|
||||
|
||||
class Validation
|
||||
{
|
||||
//--------------------------------------------------------------------
|
||||
|
|
@ -12,22 +20,23 @@ class Validation
|
|||
* Stores the classes that contain the
|
||||
* rules that are available.
|
||||
*
|
||||
* @var array
|
||||
* @var string[]
|
||||
*/
|
||||
public $ruleSets = [
|
||||
\CodeIgniter\Validation\Rules::class,
|
||||
\CodeIgniter\Validation\FormatRules::class,
|
||||
\CodeIgniter\Validation\CreditCardRules::class,
|
||||
\App\Validation\Rules::class,
|
||||
\App\Validation\FileRules::class,
|
||||
\Myth\Auth\Authentication\Passwords\ValidationRules::class,
|
||||
Rules::class,
|
||||
FormatRules::class,
|
||||
FileRules::class,
|
||||
CreditCardRules::class,
|
||||
AppRules::class,
|
||||
AppFileRules::class,
|
||||
PasswordRules::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Specifies the views that are used to display the
|
||||
* errors.
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $templates = [
|
||||
'list' => 'CodeIgniter\Validation\Views\list',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
class View extends \CodeIgniter\Config\View
|
||||
use CodeIgniter\Config\View as BaseView;
|
||||
|
||||
class View extends BaseView
|
||||
{
|
||||
/**
|
||||
* When false, the view method will clear the data between each
|
||||
|
|
@ -11,6 +13,8 @@ class View extends \CodeIgniter\Config\View
|
|||
* to each view. You might prefer to have the data stick around between
|
||||
* calls so that it is available to all views. If that is the case,
|
||||
* set $saveData to true.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $saveData = true;
|
||||
|
||||
|
|
@ -24,6 +28,8 @@ class View extends \CodeIgniter\Config\View
|
|||
* Examples:
|
||||
* { title|esc(js) }
|
||||
* { created_on|date(Y-m-d)|esc(attr) }
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $filters = [];
|
||||
|
||||
|
|
@ -31,6 +37,8 @@ class View extends \CodeIgniter\Config\View
|
|||
* Parser Plugins provide a way to extend the functionality provided
|
||||
* by the core Parser by creating aliases that will be replaced with
|
||||
* any callable. Can be single or tag pair.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $plugins = [];
|
||||
}
|
||||
|
|
|
|||
22
app/Controllers/Actor.php
Normal file
22
app/Controllers/Actor.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class Actor extends \ActivityPub\Controllers\ActorController
|
||||
{
|
||||
public function follow()
|
||||
{
|
||||
helper(['form', 'components', 'svg']);
|
||||
$data = [
|
||||
'actor' => $this->actor,
|
||||
];
|
||||
|
||||
return view('podcast/follow', $data);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class BaseController
|
||||
*
|
||||
|
|
@ -11,12 +16,8 @@ namespace App\Controllers\Admin;
|
|||
* class Home extends BaseController
|
||||
*
|
||||
* For security be sure to declare any new methods as protected or private.
|
||||
*
|
||||
* @package CodeIgniter
|
||||
*/
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class BaseController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
@ -30,11 +31,15 @@ class BaseController extends Controller
|
|||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function initController(
|
||||
\CodeIgniter\HTTP\RequestInterface $request,
|
||||
\CodeIgniter\HTTP\ResponseInterface $response,
|
||||
\Psr\Log\LoggerInterface $logger
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
// Do Not Edit This Line
|
||||
parent::initController($request, $response, $logger);
|
||||
|
|
@ -42,7 +47,6 @@ class BaseController extends Controller
|
|||
//--------------------------------------------------------------------
|
||||
// Preload any models, libraries, etc, here.
|
||||
//--------------------------------------------------------------------
|
||||
// E.g.:
|
||||
// $this->session = \Config\Services::session();
|
||||
// E.g.: $this->session = \Config\Services::session();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use App\Entities\Note;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\NoteModel;
|
||||
use App\Models\PodcastModel;
|
||||
use App\Models\SoundbiteModel;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
|
@ -32,7 +34,11 @@ class Episode extends BaseController
|
|||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
$this->podcast = (new PodcastModel())->getPodcastById($params[0]);
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())->getPodcastById($params[0]))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
if (count($params) > 1) {
|
||||
if (
|
||||
|
|
@ -107,7 +113,6 @@ class Episode extends BaseController
|
|||
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
|
||||
'transcript' => 'ext_in[transcript,txt,html,srt,json]',
|
||||
'chapters' => 'ext_in[chapters,json]',
|
||||
'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
|
|
@ -117,7 +122,6 @@ class Episode extends BaseController
|
|||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$publicationDate = $this->request->getPost('publication_date');
|
||||
$newEpisode = new \App\Entities\Episode([
|
||||
'podcast_id' => $this->podcast->id,
|
||||
'title' => $this->request->getPost('title'),
|
||||
|
|
@ -142,15 +146,9 @@ class Episode extends BaseController
|
|||
'type' => $this->request->getPost('type'),
|
||||
'is_blocked' => $this->request->getPost('block') == 'yes',
|
||||
'custom_rss_string' => $this->request->getPost('custom_rss'),
|
||||
'created_by' => user(),
|
||||
'updated_by' => user(),
|
||||
'published_at' => $publicationDate
|
||||
? Time::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$publicationDate,
|
||||
$this->request->getPost('client_timezone')
|
||||
)->setTimezone('UTC')
|
||||
: null,
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$episodeModel = new EpisodeModel();
|
||||
|
|
@ -167,7 +165,7 @@ class Episode extends BaseController
|
|||
|
||||
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
|
||||
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
|
||||
'description_footer'
|
||||
'description_footer',
|
||||
);
|
||||
|
||||
if (!$podcastModel->update($this->podcast->id, $this->podcast)) {
|
||||
|
|
@ -209,7 +207,6 @@ class Episode extends BaseController
|
|||
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
|
||||
'transcript' => 'ext_in[transcript,txt,html,srt,json]',
|
||||
'chapters' => 'ext_in[chapters,json]',
|
||||
'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
|
|
@ -222,7 +219,7 @@ class Episode extends BaseController
|
|||
$this->episode->title = $this->request->getPost('title');
|
||||
$this->episode->slug = $this->request->getPost('slug');
|
||||
$this->episode->description_markdown = $this->request->getPost(
|
||||
'description'
|
||||
'description',
|
||||
);
|
||||
$this->episode->location = $this->request->getPost('location_name');
|
||||
$this->episode->parental_advisory =
|
||||
|
|
@ -238,19 +235,10 @@ class Episode extends BaseController
|
|||
$this->episode->type = $this->request->getPost('type');
|
||||
$this->episode->is_blocked = $this->request->getPost('block') == 'yes';
|
||||
$this->episode->custom_rss_string = $this->request->getPost(
|
||||
'custom_rss'
|
||||
'custom_rss',
|
||||
);
|
||||
|
||||
$publicationDate = $this->request->getPost('publication_date');
|
||||
$this->episode->published_at = $publicationDate
|
||||
? Time::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$publicationDate,
|
||||
$this->request->getPost('client_timezone')
|
||||
)->setTimezone('UTC')
|
||||
: null;
|
||||
|
||||
$this->episode->updated_by = user();
|
||||
$this->episode->updated_by = user()->id;
|
||||
|
||||
$enclosure = $this->request->getFile('enclosure');
|
||||
if ($enclosure->isValid()) {
|
||||
|
|
@ -280,7 +268,7 @@ class Episode extends BaseController
|
|||
|
||||
// update podcast's episode_description_footer_markdown if changed
|
||||
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
|
||||
'description_footer'
|
||||
'description_footer',
|
||||
);
|
||||
|
||||
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
|
||||
|
|
@ -333,6 +321,271 @@ class Episode extends BaseController
|
|||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function publish()
|
||||
{
|
||||
if ($this->episode->publication_status === 'not_published') {
|
||||
helper(['form']);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
]);
|
||||
return view('admin/episode/publish', $data);
|
||||
} else {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
public function attemptPublish()
|
||||
{
|
||||
$rules = [
|
||||
'publication_method' => 'required',
|
||||
'scheduled_publication_date' =>
|
||||
'valid_date[Y-m-d H:i]|permit_empty',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$newNote = new Note([
|
||||
'actor_id' => $this->podcast->actor_id,
|
||||
'episode_id' => $this->episode->id,
|
||||
'message' => $this->request->getPost('message'),
|
||||
'created_by' => user_id(),
|
||||
]);
|
||||
|
||||
$publishMethod = $this->request->getPost('publication_method');
|
||||
if ($publishMethod === 'schedule') {
|
||||
$scheduledPublicationDate = $this->request->getPost(
|
||||
'scheduled_publication_date',
|
||||
);
|
||||
if ($scheduledPublicationDate) {
|
||||
$scheduledDateUTC = Time::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$scheduledPublicationDate,
|
||||
$this->request->getPost('client_timezone'),
|
||||
)->setTimezone('UTC');
|
||||
$this->episode->published_at = $scheduledDateUTC;
|
||||
$newNote->published_at = $scheduledDateUTC;
|
||||
} else {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('error', 'Schedule date must be set!');
|
||||
}
|
||||
} else {
|
||||
$dateNow = Time::now();
|
||||
$this->episode->published_at = $dateNow;
|
||||
$newNote->published_at = $dateNow;
|
||||
}
|
||||
|
||||
$noteModel = new NoteModel();
|
||||
if (!$noteModel->addNote($newNote)) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $noteModel->errors());
|
||||
}
|
||||
|
||||
$episodeModel = new EpisodeModel();
|
||||
if (!$episodeModel->update($this->episode->id, $this->episode)) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $episodeModel->errors());
|
||||
}
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
return redirect()->route('episode-view', [
|
||||
$this->podcast->id,
|
||||
$this->episode->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function publishEdit()
|
||||
{
|
||||
if ($this->episode->publication_status === 'scheduled') {
|
||||
helper(['form']);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'note' => (new NoteModel())
|
||||
->where([
|
||||
'actor_id' => $this->podcast->actor_id,
|
||||
'episode_id' => $this->episode->id,
|
||||
])
|
||||
->first(),
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
]);
|
||||
return view('admin/episode/publish_edit', $data);
|
||||
} else {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
public function attemptPublishEdit()
|
||||
{
|
||||
$rules = [
|
||||
'note_id' => 'required',
|
||||
'publication_method' => 'required',
|
||||
'scheduled_publication_date' =>
|
||||
'valid_date[Y-m-d H:i]|permit_empty',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$note = (new NoteModel())->getNoteById(
|
||||
$this->request->getPost('note_id'),
|
||||
);
|
||||
$note->message = $this->request->getPost('message');
|
||||
|
||||
$publishMethod = $this->request->getPost('publication_method');
|
||||
if ($publishMethod === 'schedule') {
|
||||
$scheduledPublicationDate = $this->request->getPost(
|
||||
'scheduled_publication_date',
|
||||
);
|
||||
if ($scheduledPublicationDate) {
|
||||
$scheduledDateUTC = Time::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$scheduledPublicationDate,
|
||||
$this->request->getPost('client_timezone'),
|
||||
)->setTimezone('UTC');
|
||||
$this->episode->published_at = $scheduledDateUTC;
|
||||
$note->published_at = $scheduledDateUTC;
|
||||
} else {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('error', 'Schedule date must be set!');
|
||||
}
|
||||
} else {
|
||||
$dateNow = Time::now();
|
||||
$this->episode->published_at = $dateNow;
|
||||
$note->published_at = $dateNow;
|
||||
}
|
||||
|
||||
$noteModel = new NoteModel();
|
||||
if (!$noteModel->editNote($note)) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $noteModel->errors());
|
||||
}
|
||||
|
||||
$episodeModel = new EpisodeModel();
|
||||
if (!$episodeModel->update($this->episode->id, $this->episode)) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $episodeModel->errors());
|
||||
}
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
return redirect()->route('episode-view', [
|
||||
$this->podcast->id,
|
||||
$this->episode->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function unpublish()
|
||||
{
|
||||
if ($this->episode->publication_status === 'published') {
|
||||
helper(['form']);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
]);
|
||||
return view('admin/episode/unpublish', $data);
|
||||
} else {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
public function attemptUnpublish()
|
||||
{
|
||||
$rules = [
|
||||
'understand' => 'required',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$db->transStart();
|
||||
|
||||
$allNotesLinkedToEpisode = (new NoteModel())
|
||||
->where([
|
||||
'episode_id' => $this->episode->id,
|
||||
])
|
||||
->findAll();
|
||||
foreach ($allNotesLinkedToEpisode as $note) {
|
||||
(new NoteModel())->removeNote($note);
|
||||
}
|
||||
|
||||
// set episode published_at to null to unpublish
|
||||
$this->episode->published_at = null;
|
||||
|
||||
$episodeModel = new EpisodeModel();
|
||||
if (!$episodeModel->update($this->episode->id, $this->episode)) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $episodeModel->errors());
|
||||
}
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
return redirect()->route('episode-view', [
|
||||
$this->podcast->id,
|
||||
$this->episode->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
(new EpisodeModel())->delete($this->episode->id);
|
||||
|
|
@ -416,7 +669,7 @@ class Episode extends BaseController
|
|||
(new SoundbiteModel())->deleteSoundbite(
|
||||
$this->podcast->id,
|
||||
$this->episode->id,
|
||||
$soundbiteId
|
||||
$soundbiteId,
|
||||
);
|
||||
|
||||
return redirect()->route('soundbites-edit', [
|
||||
|
|
|
|||
41
app/Controllers/Admin/Fediverse.php
Normal file
41
app/Controllers/Admin/Fediverse.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
use ActivityPub\Models\BlockedDomainModel;
|
||||
|
||||
class Fediverse extends BaseController
|
||||
{
|
||||
public function dashboard()
|
||||
{
|
||||
return view('admin/fediverse/dashboard');
|
||||
}
|
||||
|
||||
public function blockedActors()
|
||||
{
|
||||
helper(['form']);
|
||||
|
||||
$blockedActors = model('ActorModel')->getBlockedActors();
|
||||
|
||||
return view('admin/fediverse/blocked_actors', [
|
||||
'blockedActors' => $blockedActors,
|
||||
]);
|
||||
}
|
||||
|
||||
public function blockedDomains()
|
||||
{
|
||||
helper(['form']);
|
||||
|
||||
$blockedDomains = model('BlockedDomainModel')->getBlockedDomains();
|
||||
|
||||
return view('admin/fediverse/blocked_domains', [
|
||||
'blockedDomains' => $blockedDomains,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ class Person extends BaseController
|
|||
$this->person->image = $image;
|
||||
}
|
||||
|
||||
$this->updated_by = user();
|
||||
$this->updated_by = user()->id;
|
||||
|
||||
$personModel = new PersonModel();
|
||||
if (!$personModel->update($this->person->id, $this->person)) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class Podcast extends BaseController
|
|||
if (count($params) > 0) {
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())->getPodcastById(
|
||||
$params[0]
|
||||
$params[0],
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
|
|
@ -124,7 +124,7 @@ class Podcast extends BaseController
|
|||
'languageOptions' => $languageOptions,
|
||||
'categoryOptions' => $categoryOptions,
|
||||
'browserLang' => get_browser_language(
|
||||
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
|
||||
$this->request->getServer('HTTP_ACCEPT_LANGUAGE'),
|
||||
),
|
||||
];
|
||||
|
||||
|
|
@ -170,8 +170,8 @@ class Podcast extends BaseController
|
|||
'is_blocked' => $this->request->getPost('block') === 'yes',
|
||||
'is_completed' => $this->request->getPost('complete') === 'yes',
|
||||
'is_locked' => $this->request->getPost('lock') === 'yes',
|
||||
'created_by' => user(),
|
||||
'updated_by' => user(),
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
]);
|
||||
|
||||
$podcastModel = new PodcastModel();
|
||||
|
|
@ -193,15 +193,19 @@ class Podcast extends BaseController
|
|||
$podcastModel->addPodcastContributor(
|
||||
user()->id,
|
||||
$newPodcastId,
|
||||
$podcastAdminGroup->id
|
||||
$podcastAdminGroup->id,
|
||||
);
|
||||
|
||||
// set Podcast categories
|
||||
(new CategoryModel())->setPodcastCategories(
|
||||
$newPodcastId,
|
||||
$this->request->getPost('other_categories')
|
||||
$this->request->getPost('other_categories'),
|
||||
);
|
||||
|
||||
// set interact as the newly created podcast actor
|
||||
$createdPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
|
||||
set_interact_as_actor($createdPodcast->actor_id);
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
return redirect()->route('podcast-view', [$newPodcastId]);
|
||||
|
|
@ -239,9 +243,8 @@ class Podcast extends BaseController
|
|||
}
|
||||
|
||||
$this->podcast->title = $this->request->getPost('title');
|
||||
$this->podcast->name = $this->request->getPost('name');
|
||||
$this->podcast->description_markdown = $this->request->getPost(
|
||||
'description'
|
||||
'description',
|
||||
);
|
||||
|
||||
$image = $this->request->getFile('image');
|
||||
|
|
@ -261,10 +264,10 @@ class Podcast extends BaseController
|
|||
$this->podcast->copyright = $this->request->getPost('copyright');
|
||||
$this->podcast->location = $this->request->getPost('location_name');
|
||||
$this->podcast->payment_pointer = $this->request->getPost(
|
||||
'payment_pointer'
|
||||
'payment_pointer',
|
||||
);
|
||||
$this->podcast->custom_rss_string = $this->request->getPost(
|
||||
'custom_rss'
|
||||
'custom_rss',
|
||||
);
|
||||
$this->podcast->partner_id = $this->request->getPost('partner_id');
|
||||
$this->podcast->partner_link_url = $this->request->getPost(
|
||||
|
|
@ -277,7 +280,7 @@ class Podcast extends BaseController
|
|||
$this->podcast->is_completed =
|
||||
$this->request->getPost('complete') === 'yes';
|
||||
$this->podcast->is_locked = $this->request->getPost('lock') === 'yes';
|
||||
$this->updated_by = user();
|
||||
$this->updated_by = user()->id;
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
|
@ -294,7 +297,7 @@ class Podcast extends BaseController
|
|||
// set Podcast categories
|
||||
(new CategoryModel())->setPodcastCategories(
|
||||
$this->podcast->id,
|
||||
$this->request->getPost('other_categories')
|
||||
$this->request->getPost('other_categories'),
|
||||
);
|
||||
|
||||
$db->transComplete();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class PodcastImport extends BaseController
|
|||
if (count($params) > 0) {
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())->getPodcastById(
|
||||
$params[0]
|
||||
$params[0],
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
|
|
@ -52,7 +52,7 @@ class PodcastImport extends BaseController
|
|||
'languageOptions' => $languageOptions,
|
||||
'categoryOptions' => $categoryOptions,
|
||||
'browserLang' => get_browser_language(
|
||||
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
|
||||
$this->request->getServer('HTTP_ACCEPT_LANGUAGE'),
|
||||
),
|
||||
];
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ class PodcastImport extends BaseController
|
|||
try {
|
||||
ini_set('user_agent', 'Castopod/' . CP_VERSION);
|
||||
$feed = simplexml_load_file(
|
||||
$this->request->getPost('imported_feed_url')
|
||||
$this->request->getPost('imported_feed_url'),
|
||||
);
|
||||
} catch (\ErrorException $ex) {
|
||||
return redirect()
|
||||
|
|
@ -94,13 +94,13 @@ class PodcastImport extends BaseController
|
|||
]);
|
||||
}
|
||||
$nsItunes = $feed->channel[0]->children(
|
||||
'http://www.itunes.com/dtds/podcast-1.0.dtd'
|
||||
'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
||||
);
|
||||
$nsPodcast = $feed->channel[0]->children(
|
||||
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'
|
||||
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
|
||||
);
|
||||
$nsContent = $feed->channel[0]->children(
|
||||
'http://purl.org/rss/1.0/modules/content/'
|
||||
'http://purl.org/rss/1.0/modules/content/',
|
||||
);
|
||||
|
||||
if ((string) $nsPodcast->locked === 'yes') {
|
||||
|
|
@ -112,28 +112,30 @@ class PodcastImport extends BaseController
|
|||
|
||||
$converter = new HtmlConverter();
|
||||
|
||||
$channelDescriptionHtml = $feed->channel[0]->description;
|
||||
$channelDescriptionHtml = (string) $feed->channel[0]->description;
|
||||
|
||||
try {
|
||||
$podcast = new \App\Entities\Podcast([
|
||||
'name' => $this->request->getPost('name'),
|
||||
'imported_feed_url' => $this->request->getPost(
|
||||
'imported_feed_url'
|
||||
'imported_feed_url',
|
||||
),
|
||||
'new_feed_url' => base_url(
|
||||
route_to('podcast_feed', $this->request->getPost('name'))
|
||||
route_to('podcast_feed', $this->request->getPost('name')),
|
||||
),
|
||||
'title' => $feed->channel[0]->title,
|
||||
'title' => (string) $feed->channel[0]->title,
|
||||
'description_markdown' => $converter->convert(
|
||||
$channelDescriptionHtml
|
||||
$channelDescriptionHtml,
|
||||
),
|
||||
'description_html' => $channelDescriptionHtml,
|
||||
'image' =>
|
||||
$nsItunes->image && !empty($nsItunes->image->attributes())
|
||||
? download_file($nsItunes->image->attributes())
|
||||
? download_file((string) $nsItunes->image->attributes())
|
||||
: ($feed->channel[0]->image &&
|
||||
!empty($feed->channel[0]->image->url)
|
||||
? download_file($feed->channel[0]->image->url)
|
||||
? download_file(
|
||||
(string) $feed->channel[0]->image->url,
|
||||
)
|
||||
: null),
|
||||
'language_code' => $this->request->getPost('language'),
|
||||
'category_id' => $this->request->getPost('category'),
|
||||
|
|
@ -144,11 +146,11 @@ class PodcastImport extends BaseController
|
|||
: (in_array($nsItunes->explicit, ['no', 'false'])
|
||||
? 'clean'
|
||||
: null)),
|
||||
'owner_name' => $nsItunes->owner->name,
|
||||
'owner_email' => $nsItunes->owner->email,
|
||||
'publisher' => $nsItunes->author,
|
||||
'owner_name' => (string) $nsItunes->owner->name,
|
||||
'owner_email' => (string) $nsItunes->owner->email,
|
||||
'publisher' => (string) $nsItunes->author,
|
||||
'type' => empty($nsItunes->type) ? 'episodic' : $nsItunes->type,
|
||||
'copyright' => $feed->channel[0]->copyright,
|
||||
'copyright' => (string) $feed->channel[0]->copyright,
|
||||
'is_blocked' => empty($nsItunes->block)
|
||||
? false
|
||||
: $nsItunes->block === 'yes',
|
||||
|
|
@ -157,19 +159,19 @@ class PodcastImport extends BaseController
|
|||
: $nsItunes->complete === 'yes',
|
||||
'location_name' => !$nsPodcast->location
|
||||
? null
|
||||
: $nsPodcast->location,
|
||||
: (string) $nsPodcast->location,
|
||||
'location_geo' =>
|
||||
!$nsPodcast->location ||
|
||||
empty($nsPodcast->location->attributes()['geo'])
|
||||
? null
|
||||
: $nsPodcast->location->attributes()['geo'],
|
||||
: (string) $nsPodcast->location->attributes()['geo'],
|
||||
'location_osmid' =>
|
||||
!$nsPodcast->location ||
|
||||
empty($nsPodcast->location->attributes()['osm'])
|
||||
? null
|
||||
: $nsPodcast->location->attributes()['osm'],
|
||||
'created_by' => user(),
|
||||
'updated_by' => user(),
|
||||
: (string) $nsPodcast->location->attributes()['osm'],
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
]);
|
||||
} catch (\ErrorException $ex) {
|
||||
return redirect()
|
||||
|
|
@ -204,7 +206,7 @@ class PodcastImport extends BaseController
|
|||
$podcastModel->addPodcastContributor(
|
||||
user()->id,
|
||||
$newPodcastId,
|
||||
$podcastAdminGroup->id
|
||||
$podcastAdminGroup->id,
|
||||
);
|
||||
|
||||
$podcastsPlatformsData = [];
|
||||
|
|
@ -218,34 +220,21 @@ class PodcastImport extends BaseController
|
|||
foreach ($platformType['elements'] as $platform) {
|
||||
$platformLabel = $platform->attributes()['platform'];
|
||||
$platformSlug = slugify($platformLabel);
|
||||
if (!$platformModel->getPlatform($platformSlug)) {
|
||||
if (
|
||||
!$platformModel->createPlatform(
|
||||
$platformSlug,
|
||||
$platformType['name'],
|
||||
$platformLabel,
|
||||
''
|
||||
)
|
||||
) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $platformModel->errors());
|
||||
}
|
||||
if ($platformModel->getPlatform($platformSlug)) {
|
||||
array_push($podcastsPlatformsData, [
|
||||
'platform_slug' => $platformSlug,
|
||||
'podcast_id' => $newPodcastId,
|
||||
'link_url' => $platform->attributes()['url'],
|
||||
'link_content' => $platform->attributes()['id'],
|
||||
'is_visible' => false,
|
||||
]);
|
||||
}
|
||||
array_push($podcastsPlatformsData, [
|
||||
'platform_slug' => $platformSlug,
|
||||
'podcast_id' => $newPodcastId,
|
||||
'link_url' => $platform->attributes()['url'],
|
||||
'link_content' => $platform->attributes()['id'],
|
||||
'is_visible' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (count($podcastsPlatformsData) > 1) {
|
||||
$platformModel->createPodcastPlatforms(
|
||||
$newPodcastId,
|
||||
$podcastsPlatformsData
|
||||
$podcastsPlatformsData,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -259,7 +248,7 @@ class PodcastImport extends BaseController
|
|||
!($newPersonId = $personModel->createPerson(
|
||||
$podcastPerson,
|
||||
$podcastPerson->attributes()['href'],
|
||||
$podcastPerson->attributes()['img']
|
||||
$podcastPerson->attributes()['img'],
|
||||
))
|
||||
) {
|
||||
return redirect()
|
||||
|
|
@ -312,19 +301,19 @@ class PodcastImport extends BaseController
|
|||
$item = $feed->channel[0]->item[$numberItems - $itemNumber];
|
||||
|
||||
$nsItunes = $item->children(
|
||||
'http://www.itunes.com/dtds/podcast-1.0.dtd'
|
||||
'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
||||
);
|
||||
$nsPodcast = $item->children(
|
||||
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'
|
||||
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
|
||||
);
|
||||
$nsContent = $item->children(
|
||||
'http://purl.org/rss/1.0/modules/content/'
|
||||
'http://purl.org/rss/1.0/modules/content/',
|
||||
);
|
||||
|
||||
$slug = slugify(
|
||||
$this->request->getPost('slug_field') === 'title'
|
||||
? $item->title
|
||||
: basename($item->link)
|
||||
: basename($item->link),
|
||||
);
|
||||
if (in_array($slug, $slugs)) {
|
||||
$slugNumber = 2;
|
||||
|
|
@ -358,13 +347,15 @@ class PodcastImport extends BaseController
|
|||
'slug' => $slug,
|
||||
'enclosure' => download_file($item->enclosure->attributes()),
|
||||
'description_markdown' => $converter->convert(
|
||||
$itemDescriptionHtml
|
||||
$itemDescriptionHtml,
|
||||
),
|
||||
'description_html' => $itemDescriptionHtml,
|
||||
'image' =>
|
||||
!$nsItunes->image || empty($nsItunes->image->attributes())
|
||||
? null
|
||||
: download_file($nsItunes->image->attributes()),
|
||||
: download_file(
|
||||
(string) $nsItunes->image->attributes(),
|
||||
),
|
||||
'parental_advisory' => empty($nsItunes->explicit)
|
||||
? null
|
||||
: (in_array($nsItunes->explicit, ['yes', 'true'])
|
||||
|
|
@ -404,8 +395,8 @@ class PodcastImport extends BaseController
|
|||
empty($nsPodcast->location->attributes()['osm'])
|
||||
? null
|
||||
: $nsPodcast->location->attributes()['osm'],
|
||||
'created_by' => user(),
|
||||
'updated_by' => user(),
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
'published_at' => strtotime($item->pubDate),
|
||||
]);
|
||||
|
||||
|
|
@ -429,7 +420,7 @@ class PodcastImport extends BaseController
|
|||
!($newPersonId = $personModel->createPerson(
|
||||
$episodePerson,
|
||||
$episodePerson->attributes()['href'],
|
||||
$episodePerson->attributes()['img']
|
||||
$episodePerson->attributes()['img'],
|
||||
))
|
||||
) {
|
||||
return redirect()
|
||||
|
|
@ -458,8 +449,8 @@ class PodcastImport extends BaseController
|
|||
'person_group' => $personGroup['slug'],
|
||||
'person_role' => $personRole['slug'],
|
||||
]);
|
||||
$episodePersonModel = new EpisodePersonModel();
|
||||
|
||||
$episodePersonModel = new EpisodePersonModel();
|
||||
if (!$episodePersonModel->insert($newEpisodePerson)) {
|
||||
return redirect()
|
||||
->back()
|
||||
|
|
@ -469,6 +460,10 @@ class PodcastImport extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
// set interact as the newly imported podcast actor
|
||||
$importedPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
|
||||
set_interact_as_actor($importedPodcast->actor_id);
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
return redirect()->route('podcast-view', [$newPodcastId]);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class Auth extends \Myth\Auth\Controllers\AuthController
|
|||
$allowedPostFields = array_merge(
|
||||
['password'],
|
||||
$this->config->validFields,
|
||||
$this->config->personalFields
|
||||
$this->config->personalFields,
|
||||
);
|
||||
$user = new User($this->request->getPost($allowedPostFields));
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ class Auth extends \Myth\Auth\Controllers\AuthController
|
|||
->withInput()
|
||||
->with(
|
||||
'error',
|
||||
$activator->error() ?? lang('Auth.unknownError')
|
||||
$activator->error() ?? lang('Auth.unknownError'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ class Auth extends \Myth\Auth\Controllers\AuthController
|
|||
$this->request->getPost('email'),
|
||||
$this->request->getPost('token'),
|
||||
$this->request->getIPAddress(),
|
||||
(string) $this->request->getUserAgent()
|
||||
(string) $this->request->getUserAgent(),
|
||||
);
|
||||
|
||||
$rules = [
|
||||
|
|
@ -172,4 +172,24 @@ class Auth extends \Myth\Auth\Controllers\AuthController
|
|||
->route('login')
|
||||
->with('message', lang('Auth.resetSuccess'));
|
||||
}
|
||||
|
||||
public function attemptInteractAsActor()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|numeric',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', service('validation')->getErrors());
|
||||
}
|
||||
|
||||
helper('auth');
|
||||
|
||||
set_interact_as_actor($this->request->getPost('actor_id'));
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class BaseController
|
||||
*
|
||||
|
|
@ -9,14 +16,7 @@
|
|||
* class Home extends BaseController
|
||||
*
|
||||
* For security be sure to declare any new methods as protected or private.
|
||||
*
|
||||
* @package CodeIgniter
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class BaseController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
@ -26,15 +26,19 @@ class BaseController extends Controller
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $helpers = ['analytics', 'svg', 'components', 'misc'];
|
||||
protected $helpers = ['auth', 'analytics', 'svg', 'components', 'misc'];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function initController(
|
||||
\CodeIgniter\HTTP\RequestInterface $request,
|
||||
\CodeIgniter\HTTP\ResponseInterface $response,
|
||||
\Psr\Log\LoggerInterface $logger
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
// Do Not Edit This Line
|
||||
parent::initController($request, $response, $logger);
|
||||
|
|
@ -42,8 +46,7 @@ class BaseController extends Controller
|
|||
//--------------------------------------------------------------------
|
||||
// Preload any models, libraries, etc, here.
|
||||
//--------------------------------------------------------------------
|
||||
// E.g.:
|
||||
// $this->session = \Config\Services::session();
|
||||
// E.g.: $this->session = \Config\Services::session();
|
||||
|
||||
set_user_session_deny_list_ip();
|
||||
set_user_session_browser();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace App\Controllers;
|
|||
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
use SimpleXMLElement;
|
||||
|
||||
class Episode extends BaseController
|
||||
{
|
||||
|
|
@ -31,7 +32,7 @@ class Episode extends BaseController
|
|||
count($params) > 1 &&
|
||||
!($this->episode = (new EpisodeModel())->getEpisodeBySlug(
|
||||
$this->podcast->id,
|
||||
$params[1]
|
||||
$params[1],
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
|
|
@ -43,44 +44,49 @@ class Episode extends BaseController
|
|||
|
||||
public function index()
|
||||
{
|
||||
self::triggerWebpageHit($this->episode->podcast_id);
|
||||
$episodeModel = new EpisodeModel();
|
||||
|
||||
self::triggerWebpageHit($this->podcast->id);
|
||||
|
||||
$locale = service('request')->getLocale();
|
||||
$cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_{$locale}";
|
||||
|
||||
if (!($cachedView = cache($cacheName))) {
|
||||
$episodeModel = new EpisodeModel();
|
||||
$previousNextEpisodes = $episodeModel->getPreviousNextEpisodes(
|
||||
$this->episode,
|
||||
$this->podcast->type
|
||||
);
|
||||
|
||||
helper(['persons']);
|
||||
$persons = [];
|
||||
construct_episode_person_array(
|
||||
$this->episode->episode_persons,
|
||||
$persons
|
||||
);
|
||||
helper('persons');
|
||||
$episodePersons = [];
|
||||
construct_person_array($this->episode->persons, $episodePersons);
|
||||
$podcastPersons = [];
|
||||
construct_person_array($this->podcast->persons, $podcastPersons);
|
||||
|
||||
$data = [
|
||||
'previousEpisode' => $previousNextEpisodes['previous'],
|
||||
'nextEpisode' => $previousNextEpisodes['next'],
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'persons' => $persons,
|
||||
'episodePersons' => $episodePersons,
|
||||
'persons' => $podcastPersons,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id
|
||||
$this->podcast->id,
|
||||
);
|
||||
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
return view('episode', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
if (can_user_interact()) {
|
||||
helper('form');
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
return view('podcast/episode_authenticated', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName . '_authenticated',
|
||||
]);
|
||||
} else {
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
return view('podcast/episode', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $cachedView;
|
||||
|
|
@ -97,7 +103,7 @@ class Episode extends BaseController
|
|||
if (isset($_SERVER['HTTP_REFERER'])) {
|
||||
$session->set(
|
||||
'embeddable_player_domain',
|
||||
parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
|
||||
parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -108,26 +114,15 @@ class Episode extends BaseController
|
|||
if (!($cachedView = cache($cacheName))) {
|
||||
$episodeModel = new EpisodeModel();
|
||||
$theme = EpisodeModel::$themes[$theme];
|
||||
helper(['persons']);
|
||||
$persons = [];
|
||||
construct_episode_person_array(
|
||||
$this->episode->episode_persons,
|
||||
$persons
|
||||
);
|
||||
constructs_podcast_person_array(
|
||||
$this->podcast->podcast_persons,
|
||||
$persons
|
||||
);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'persons' => $persons,
|
||||
'theme' => $theme,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id
|
||||
$this->podcast->id,
|
||||
);
|
||||
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
|
|
@ -141,4 +136,56 @@ class Episode extends BaseController
|
|||
|
||||
return $cachedView;
|
||||
}
|
||||
|
||||
public function oembedJSON()
|
||||
{
|
||||
return $this->response->setJSON([
|
||||
'type' => 'rich',
|
||||
'version' => '1.0',
|
||||
'title' => $this->episode->title,
|
||||
'provider_name' => $this->podcast->title,
|
||||
'provider_url' => $this->podcast->link,
|
||||
'author_name' => $this->podcast->title,
|
||||
'author_url' => $this->podcast->link,
|
||||
'html' =>
|
||||
'<iframe src="' .
|
||||
$this->episode->embeddable_player .
|
||||
'" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
|
||||
'width' => 600,
|
||||
'height' => 200,
|
||||
'thumbnail_url' => $this->episode->image->large_url,
|
||||
'thumbnail_width' => config('Images')->largeSize,
|
||||
'thumbnail_height' => config('Images')->largeSize,
|
||||
]);
|
||||
}
|
||||
|
||||
public function oembedXML()
|
||||
{
|
||||
$oembed = new SimpleXMLElement(
|
||||
"<?xml version='1.0' encoding='utf-8' standalone='yes'?><oembed></oembed>",
|
||||
);
|
||||
|
||||
$oembed->addChild('type', 'rich');
|
||||
$oembed->addChild('version', '1.0');
|
||||
$oembed->addChild('title', $this->episode->title);
|
||||
$oembed->addChild('provider_name', $this->podcast->title);
|
||||
$oembed->addChild('provider_url', $this->podcast->link);
|
||||
$oembed->addChild('author_name', $this->podcast->title);
|
||||
$oembed->addChild('author_url', $this->podcast->link);
|
||||
$oembed->addChild('thumbnail', $this->episode->image->large_url);
|
||||
$oembed->addChild('thumbnail_width', config('Images')->largeSize);
|
||||
$oembed->addChild('thumbnail_height', config('Images')->largeSize);
|
||||
$oembed->addChild(
|
||||
'html',
|
||||
htmlentities(
|
||||
'<iframe src="' .
|
||||
$this->episode->embeddable_player .
|
||||
'" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
|
||||
),
|
||||
);
|
||||
$oembed->addChild('width', 600);
|
||||
$oembed->addChild('height', 200);
|
||||
|
||||
return $this->response->setXML($oembed);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ class Home extends BaseController
|
|||
|
||||
// check if there's only one podcast to redirect user to it
|
||||
if (count($allPodcasts) == 1) {
|
||||
return redirect()->route('podcast', [$allPodcasts[0]->name]);
|
||||
return redirect()->route('podcast-activity', [
|
||||
$allPodcasts[0]->name,
|
||||
]);
|
||||
}
|
||||
|
||||
// default behavior: list all podcasts on home page
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ class Install extends Controller
|
|||
$migrations = \Config\Services::migrations();
|
||||
|
||||
!$migrations->setNamespace('Myth\Auth')->latest();
|
||||
!$migrations->setNamespace('ActivityPub')->latest();
|
||||
!$migrations->setNamespace(APP_NAMESPACE)->latest();
|
||||
}
|
||||
|
||||
|
|
|
|||
212
app/Controllers/Note.php
Normal file
212
app/Controllers/Note.php
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class Note extends \ActivityPub\Controllers\NoteController
|
||||
{
|
||||
/**
|
||||
* @var \App\Entities\Podcast
|
||||
*/
|
||||
protected $podcast;
|
||||
|
||||
protected $helpers = ['auth', 'activitypub', 'svg', 'components', 'misc'];
|
||||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())->getPodcastByName(
|
||||
$params[0],
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$this->actor = $this->podcast->actor;
|
||||
|
||||
if (count($params) > 1) {
|
||||
if (!($this->note = model('NoteModel')->getNoteById($params[1]))) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
unset($params[0]);
|
||||
unset($params[1]);
|
||||
|
||||
return $this->$method(...$params);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
helper('persons');
|
||||
$persons = [];
|
||||
construct_person_array($this->podcast->persons, $persons);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'note' => $this->note,
|
||||
'persons' => $persons,
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
if (can_user_interact()) {
|
||||
helper('form');
|
||||
return view('podcast/note_authenticated', $data);
|
||||
} else {
|
||||
return view('podcast/note', $data);
|
||||
}
|
||||
}
|
||||
|
||||
public function attemptCreate()
|
||||
{
|
||||
$rules = [
|
||||
'message' => 'required|max_length[500]',
|
||||
'episode_url' => 'valid_url|permit_empty',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$message = $this->request->getPost('message');
|
||||
|
||||
$newNote = new \App\Entities\Note([
|
||||
'actor_id' => interact_as_actor_id(),
|
||||
'published_at' => Time::now(),
|
||||
'created_by' => user_id(),
|
||||
]);
|
||||
|
||||
// get episode if episodeUrl has been set
|
||||
$episodeUri = $this->request->getPost('episode_url');
|
||||
if (
|
||||
$episodeUri &&
|
||||
($params = extract_params_from_episode_uri(new URI($episodeUri)))
|
||||
) {
|
||||
if (
|
||||
$episode = (new EpisodeModel())->getEpisodeBySlug(
|
||||
$params['podcastName'],
|
||||
$params['episodeSlug'],
|
||||
)
|
||||
) {
|
||||
$newNote->episode_id = $episode->id;
|
||||
}
|
||||
}
|
||||
|
||||
$newNote->message = $message;
|
||||
|
||||
if (
|
||||
!model('NoteModel')->addNote(
|
||||
$newNote,
|
||||
$newNote->episode_id ? false : true,
|
||||
true,
|
||||
)
|
||||
) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', model('NoteModel')->errors());
|
||||
}
|
||||
|
||||
// Note has been successfully created
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReply()
|
||||
{
|
||||
$rules = [
|
||||
'message' => 'required|max_length[500]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$newNote = new \ActivityPub\Entities\Note([
|
||||
'actor_id' => interact_as_actor_id(),
|
||||
'in_reply_to_id' => $this->note->id,
|
||||
'message' => $this->request->getPost('message'),
|
||||
'published_at' => Time::now(),
|
||||
'created_by' => user_id(),
|
||||
]);
|
||||
|
||||
if (!model('NoteModel')->addReply($newNote)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', model('NoteModel')->errors());
|
||||
}
|
||||
|
||||
// Reply note without preview card has been successfully created
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptFavourite()
|
||||
{
|
||||
model('FavouriteModel')->toggleFavourite(
|
||||
interact_as_actor(),
|
||||
$this->note,
|
||||
);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReblog()
|
||||
{
|
||||
model('NoteModel')->toggleReblog(interact_as_actor(), $this->note);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptAction()
|
||||
{
|
||||
$rules = [
|
||||
'action' => 'required|in_list[favourite,reblog,reply]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
switch ($this->request->getPost('action')) {
|
||||
case 'favourite':
|
||||
return $this->attemptFavourite();
|
||||
case 'reblog':
|
||||
return $this->attemptReblog();
|
||||
case 'reply':
|
||||
return $this->attemptReply();
|
||||
}
|
||||
}
|
||||
|
||||
public function remoteAction($action)
|
||||
{
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'note' => $this->note,
|
||||
'action' => $action,
|
||||
];
|
||||
|
||||
helper('form');
|
||||
|
||||
return view('podcast/note_remote_action', $data);
|
||||
}
|
||||
}
|
||||
|
|
@ -85,14 +85,23 @@ class Page extends BaseController
|
|||
'role_label' => $credit->role_label,
|
||||
'is_in' => [
|
||||
[
|
||||
'link' => $credit->episode
|
||||
'link' => $credit->episode_id
|
||||
? $credit->episode->link
|
||||
: $credit->podcast->link,
|
||||
'title' => $credit->episode
|
||||
'title' => $credit->episode_id
|
||||
? (count($allPodcasts) > 1
|
||||
? "{$credit->podcast->title} ▸ "
|
||||
: '') .
|
||||
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
|
||||
$credit->episode
|
||||
->title .
|
||||
episode_numbering(
|
||||
$credit->episode
|
||||
->number,
|
||||
$credit->episode
|
||||
->season_number,
|
||||
'text-xs ml-2',
|
||||
true,
|
||||
)
|
||||
: $credit->podcast->title,
|
||||
],
|
||||
],
|
||||
|
|
@ -114,14 +123,21 @@ class Page extends BaseController
|
|||
'role_label' => $credit->role_label,
|
||||
'is_in' => [
|
||||
[
|
||||
'link' => $credit->episode
|
||||
'link' => $credit->episode_id
|
||||
? $credit->episode->link
|
||||
: $credit->podcast->link,
|
||||
'title' => $credit->episode
|
||||
'title' => $credit->episode_id
|
||||
? (count($allPodcasts) > 1
|
||||
? "{$credit->podcast->title} ▸ "
|
||||
: '') .
|
||||
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
|
||||
$credit->episode->title .
|
||||
episode_numbering(
|
||||
$credit->episode->number,
|
||||
$credit->episode
|
||||
->season_number,
|
||||
'text-xs ml-2',
|
||||
true,
|
||||
)
|
||||
: $credit->podcast->title,
|
||||
],
|
||||
],
|
||||
|
|
@ -143,7 +159,13 @@ class Page extends BaseController
|
|||
? (count($allPodcasts) > 1
|
||||
? "{$credit->podcast->title} ▸ "
|
||||
: '') .
|
||||
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
|
||||
$credit->episode->title .
|
||||
episode_numbering(
|
||||
$credit->episode->number,
|
||||
$credit->episode->season_number,
|
||||
'text-xs ml-2',
|
||||
true,
|
||||
)
|
||||
: $credit->podcast->title,
|
||||
],
|
||||
],
|
||||
|
|
@ -159,7 +181,13 @@ class Page extends BaseController
|
|||
? (count($allPodcasts) > 1
|
||||
? "{$credit->podcast->title} ▸ "
|
||||
: '') .
|
||||
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
|
||||
$credit->episode->title .
|
||||
episode_numbering(
|
||||
$credit->episode->number,
|
||||
$credit->episode->season_number,
|
||||
'text-xs ml-2',
|
||||
true,
|
||||
)
|
||||
: $credit->podcast->title,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace App\Controllers;
|
|||
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
use App\Models\NoteModel;
|
||||
|
||||
class Podcast extends BaseController
|
||||
{
|
||||
|
|
@ -23,17 +24,41 @@ class Podcast extends BaseController
|
|||
if (count($params) > 0) {
|
||||
if (
|
||||
!($this->podcast = (new PodcastModel())->getPodcastByName(
|
||||
$params[0]
|
||||
$params[0],
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
unset($params[0]);
|
||||
}
|
||||
|
||||
return $this->$method();
|
||||
return $this->$method(...$params);
|
||||
}
|
||||
|
||||
public function index()
|
||||
public function activity()
|
||||
{
|
||||
helper('persons');
|
||||
$persons = [];
|
||||
construct_person_array($this->podcast->persons, $persons);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'notes' => (new NoteModel())->getActorNotes(
|
||||
$this->podcast->actor_id,
|
||||
),
|
||||
'persons' => $persons,
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
if (can_user_interact()) {
|
||||
helper('form');
|
||||
return view('podcast/activity_authenticated', $data);
|
||||
} else {
|
||||
return view('podcast/activity', $data);
|
||||
}
|
||||
}
|
||||
|
||||
public function episodes()
|
||||
{
|
||||
self::triggerWebpageHit($this->podcast->id);
|
||||
|
||||
|
|
@ -42,7 +67,7 @@ class Podcast extends BaseController
|
|||
|
||||
if (!$yearQuery and !$seasonQuery) {
|
||||
$defaultQuery = (new EpisodeModel())->getDefaultQuery(
|
||||
$this->podcast->id
|
||||
$this->podcast->id,
|
||||
);
|
||||
if ($defaultQuery['type'] == 'season') {
|
||||
$seasonQuery = $defaultQuery['data']['season_number'];
|
||||
|
|
@ -59,7 +84,7 @@ class Podcast extends BaseController
|
|||
$yearQuery,
|
||||
$seasonQuery ? 'season' . $seasonQuery : null,
|
||||
service('request')->getLocale(),
|
||||
])
|
||||
]),
|
||||
);
|
||||
|
||||
if (!($found = cache($cacheName))) {
|
||||
|
|
@ -73,14 +98,19 @@ class Podcast extends BaseController
|
|||
foreach ($years as $year) {
|
||||
$isActive = $yearQuery == $year['year'];
|
||||
if ($isActive) {
|
||||
$activeQuery = ['type' => 'year', 'value' => $year['year']];
|
||||
$activeQuery = [
|
||||
'type' => 'year',
|
||||
'value' => $year['year'],
|
||||
'label' => $year['year'],
|
||||
'number_of_episodes' => $year['number_of_episodes'],
|
||||
];
|
||||
}
|
||||
|
||||
array_push($episodesNavigation, [
|
||||
'label' => $year['year'],
|
||||
'number_of_episodes' => $year['number_of_episodes'],
|
||||
'route' =>
|
||||
route_to('podcast', $this->podcast->name) .
|
||||
route_to('podcast-episodes', $this->podcast->name) .
|
||||
'?year=' .
|
||||
$year['year'],
|
||||
'is_active' => $isActive,
|
||||
|
|
@ -93,6 +123,10 @@ class Podcast extends BaseController
|
|||
$activeQuery = [
|
||||
'type' => 'season',
|
||||
'value' => $season['season_number'],
|
||||
'label' => lang('Podcast.season', [
|
||||
'seasonNumber' => $season['season_number'],
|
||||
]),
|
||||
'number_of_episodes' => $season['number_of_episodes'],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -102,19 +136,16 @@ class Podcast extends BaseController
|
|||
]),
|
||||
'number_of_episodes' => $season['number_of_episodes'],
|
||||
'route' =>
|
||||
route_to('podcast', $this->podcast->name) .
|
||||
route_to('podcast-episodes', $this->podcast->name) .
|
||||
'?season=' .
|
||||
$season['season_number'],
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
}
|
||||
|
||||
helper(['persons']);
|
||||
helper('persons');
|
||||
$persons = [];
|
||||
constructs_podcast_person_array(
|
||||
$this->podcast->podcast_persons,
|
||||
$persons
|
||||
);
|
||||
construct_person_array($this->podcast->persons, $persons);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
|
|
@ -124,21 +155,31 @@ class Podcast extends BaseController
|
|||
$this->podcast->id,
|
||||
$this->podcast->type,
|
||||
$yearQuery,
|
||||
$seasonQuery
|
||||
$seasonQuery,
|
||||
),
|
||||
'personArray' => $persons,
|
||||
'persons' => $persons,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id
|
||||
$this->podcast->id,
|
||||
);
|
||||
|
||||
return view('podcast', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
// if user is logged in then send to the authenticated episodes view
|
||||
if (can_user_interact()) {
|
||||
return view('podcast/episodes_authenticated', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName . '_authenticated',
|
||||
]);
|
||||
} else {
|
||||
return view('podcast/episodes', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class AddCategories extends Migration
|
|||
'constraint' => 32,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey('code');
|
||||
$this->forge->addForeignKey('parent_id', 'categories', 'id');
|
||||
$this->forge->createTable('categories');
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class AddLanguages extends Migration
|
|||
'constraint' => 128,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('code', true);
|
||||
$this->forge->addPrimaryKey('code');
|
||||
$this->forge->createTable('languages');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,14 +23,17 @@ class AddPodcasts extends Migration
|
|||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 128,
|
||||
'actor_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
],
|
||||
'name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 32,
|
||||
'unique' => true,
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 128,
|
||||
],
|
||||
'description_markdown' => [
|
||||
'type' => 'TEXT',
|
||||
|
|
@ -42,6 +45,12 @@ class AddPodcasts extends Migration
|
|||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
|
||||
'image_mimetype' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 13,
|
||||
],
|
||||
'language_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 2,
|
||||
|
|
@ -140,6 +149,7 @@ class AddPodcasts extends Migration
|
|||
],
|
||||
'custom_rss' => [
|
||||
'type' => 'JSON',
|
||||
'null' => true,
|
||||
],
|
||||
'partner_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
|
|
@ -176,7 +186,15 @@ class AddPodcasts extends Migration
|
|||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey('name');
|
||||
$this->forge->addForeignKey(
|
||||
'actor_id',
|
||||
'activitypub_actors',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey('category_id', 'categories', 'id');
|
||||
$this->forge->addForeignKey('language_code', 'languages', 'code');
|
||||
$this->forge->addForeignKey('created_by', 'users', 'id');
|
||||
|
|
|
|||
|
|
@ -73,6 +73,13 @@ class AddEpisodes extends Migration
|
|||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
|
||||
'image_mimetype' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 13,
|
||||
'null' => true,
|
||||
],
|
||||
'transcript_uri' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
|
|
@ -128,6 +135,21 @@ class AddEpisodes extends Migration
|
|||
'type' => 'JSON',
|
||||
'null' => true,
|
||||
],
|
||||
'favourites_total' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'reblogs_total' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'notes_total' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'created_by' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
|
|
@ -151,9 +173,15 @@ class AddEpisodes extends Migration
|
|||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey(['podcast_id', 'slug']);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey(
|
||||
'podcast_id',
|
||||
'podcasts',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey('created_by', 'users', 'id');
|
||||
$this->forge->addForeignKey('updated_by', 'users', 'id');
|
||||
$this->forge->createTable('episodes');
|
||||
|
|
|
|||
|
|
@ -63,8 +63,20 @@ class AddSoundbites extends Migration
|
|||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey(['episode_id', 'start_time', 'duration']);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey('episode_id', 'episodes', 'id');
|
||||
$this->forge->addForeignKey(
|
||||
'podcast_id',
|
||||
'podcasts',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'episode_id',
|
||||
'episodes',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey('created_by', 'users', 'id');
|
||||
$this->forge->addForeignKey('updated_by', 'users', 'id');
|
||||
$this->forge->createTable('soundbites');
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class AddPlatforms extends Migration
|
|||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()'
|
||||
);
|
||||
$this->forge->addKey('slug', true);
|
||||
$this->forge->addPrimaryKey('slug');
|
||||
$this->forge->createTable('platforms');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsPodcasts
|
||||
* Creates analytics_podcasts table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -45,12 +46,11 @@ class AddAnalyticsPodcasts extends Migration
|
|||
]);
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'date']);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_podcasts');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsPodcastsByEpisode
|
||||
* Creates analytics_episodes_by_episode table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -41,13 +42,11 @@ class AddAnalyticsPodcastsByEpisode extends Migration
|
|||
]);
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'date', 'episode_id']);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey('episode_id', 'episodes', 'id');
|
||||
$this->forge->createTable('analytics_podcasts_by_episode');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsPodcastsByHour
|
||||
* Creates analytics_podcasts_by_hour table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -36,12 +37,11 @@ class AddAnalyticsPodcastsByHour extends Migration
|
|||
]);
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'date', 'hour']);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_podcasts_by_hour');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsPodcastsByPlayer
|
||||
* Creates analytics_podcasts_by_player table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -61,12 +62,11 @@ class AddAnalyticsPodcastsByPlayer extends Migration
|
|||
'is_bot',
|
||||
]);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_podcasts_by_player');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsPodcastsByCountry
|
||||
* Creates analytics_podcasts_by_country table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -37,12 +38,11 @@ class AddAnalyticsPodcastsByCountry extends Migration
|
|||
]);
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'date', 'country_code']);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_podcasts_by_country');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsPodcastsByRegion
|
||||
* Creates analytics_podcasts_by_region table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -55,12 +56,11 @@ class AddAnalyticsPodcastsByRegion extends Migration
|
|||
'region_code',
|
||||
]);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_podcasts_by_region');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ class AddPodcastsPlatforms extends Migration
|
|||
]);
|
||||
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey('platform_slug', 'platforms', 'slug');
|
||||
$this->forge->createTable('podcasts_platforms');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsWebsiteByBrowser
|
||||
* Creates analytics_website_by_browser table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -37,12 +38,11 @@ class AddAnalyticsWebsiteByBrowser extends Migration
|
|||
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'date', 'browser']);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_website_by_browser');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsWebsiteByReferer
|
||||
* Creates analytics_website_by_referer table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -46,12 +47,11 @@ class AddAnalyticsWebsiteByReferer extends Migration
|
|||
]);
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'date', 'referer_url']);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_website_by_referer');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsWebsiteByEntryPage
|
||||
* Creates analytics_website_by_entry_page table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -36,12 +37,11 @@ class AddAnalyticsWebsiteByEntryPage extends Migration
|
|||
]);
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'date', 'entry_page_url']);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addField(
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
|
||||
);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('analytics_website_by_entry_page');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsUnknownUseragents
|
||||
* Creates analytics_unknown_useragents table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -33,7 +34,8 @@ class AddAnalyticsUnknownUseragents extends Migration
|
|||
'default' => 1,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
|
||||
$this->forge->addPrimaryKey('id');
|
||||
// `created_at` and `updated_at` are created with SQL because Model class won’t be used for insertion (Procedure will be used instead)
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsPodcastsProcedure
|
||||
* Creates analytics_podcasts procedure in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -21,58 +22,58 @@ class AddAnalyticsPodcastsProcedure extends Migration
|
|||
$prefix = $this->db->getPrefix();
|
||||
|
||||
$createQuery = <<<EOD
|
||||
CREATE PROCEDURE `{$prefix}analytics_podcasts` (
|
||||
IN `p_podcast_id` INT UNSIGNED,
|
||||
IN `p_episode_id` INT UNSIGNED,
|
||||
IN `p_country_code` VARCHAR(3) CHARSET utf8mb4,
|
||||
IN `p_region_code` VARCHAR(3) CHARSET utf8mb4,
|
||||
IN `p_latitude` FLOAT,
|
||||
IN `p_longitude` FLOAT,
|
||||
IN `p_service` VARCHAR(128) CHARSET utf8mb4,
|
||||
IN `p_app` VARCHAR(128) CHARSET utf8mb4,
|
||||
IN `p_device` VARCHAR(32) CHARSET utf8mb4,
|
||||
IN `p_os` VARCHAR(32) CHARSET utf8mb4,
|
||||
IN `p_bot` TINYINT(1) UNSIGNED,
|
||||
IN `p_filesize` INT UNSIGNED,
|
||||
IN `p_duration` INT UNSIGNED,
|
||||
IN `p_age` INT UNSIGNED,
|
||||
IN `p_new_listener` TINYINT(1) UNSIGNED
|
||||
) MODIFIES SQL DATA
|
||||
DETERMINISTIC
|
||||
SQL SECURITY INVOKER
|
||||
COMMENT 'Add one hit in podcast logs tables.'
|
||||
BEGIN
|
||||
CREATE PROCEDURE `{$prefix}analytics_podcasts` (
|
||||
IN `p_podcast_id` INT UNSIGNED,
|
||||
IN `p_episode_id` INT UNSIGNED,
|
||||
IN `p_country_code` VARCHAR(3) CHARSET utf8mb4,
|
||||
IN `p_region_code` VARCHAR(3) CHARSET utf8mb4,
|
||||
IN `p_latitude` FLOAT,
|
||||
IN `p_longitude` FLOAT,
|
||||
IN `p_service` VARCHAR(128) CHARSET utf8mb4,
|
||||
IN `p_app` VARCHAR(128) CHARSET utf8mb4,
|
||||
IN `p_device` VARCHAR(32) CHARSET utf8mb4,
|
||||
IN `p_os` VARCHAR(32) CHARSET utf8mb4,
|
||||
IN `p_bot` TINYINT(1) UNSIGNED,
|
||||
IN `p_filesize` INT UNSIGNED,
|
||||
IN `p_duration` INT UNSIGNED,
|
||||
IN `p_age` INT UNSIGNED,
|
||||
IN `p_new_listener` TINYINT(1) UNSIGNED
|
||||
) MODIFIES SQL DATA
|
||||
DETERMINISTIC
|
||||
SQL SECURITY INVOKER
|
||||
COMMENT 'Add one hit in podcast logs tables.'
|
||||
BEGIN
|
||||
|
||||
SET @current_datetime = NOW();
|
||||
SET @current_date = DATE(@current_datetime);
|
||||
SET @current_hour = HOUR(@current_datetime);
|
||||
SET @current_datetime = NOW();
|
||||
SET @current_date = DATE(@current_datetime);
|
||||
SET @current_hour = HOUR(@current_datetime);
|
||||
|
||||
IF NOT `p_bot` THEN
|
||||
INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`)
|
||||
VALUES (p_podcast_id, @current_date)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`duration`=`duration`+`p_duration`,
|
||||
`bandwidth`=`bandwidth`+`p_filesize`,
|
||||
`hits`=`hits`+1,
|
||||
`unique_listeners`=`unique_listeners`+`p_new_listener`;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_hour`(`podcast_id`, `date`, `hour`)
|
||||
VALUES (p_podcast_id, @current_date, @current_hour)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`)
|
||||
VALUES (p_podcast_id, p_episode_id, @current_date, p_age)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_country`(`podcast_id`, `country_code`, `date`)
|
||||
VALUES (p_podcast_id, p_country_code, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_region`(`podcast_id`, `country_code`, `region_code`, `latitude`, `longitude`, `date`)
|
||||
VALUES (p_podcast_id, p_country_code, p_region_code, p_latitude, p_longitude, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
END IF;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_player`(`podcast_id`, `service`, `app`, `device`, `os`, `is_bot`, `date`)
|
||||
VALUES (p_podcast_id, p_service, p_app, p_device, p_os, p_bot, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
END
|
||||
EOD;
|
||||
IF NOT `p_bot` THEN
|
||||
INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`)
|
||||
VALUES (p_podcast_id, @current_date)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`duration`=`duration`+`p_duration`,
|
||||
`bandwidth`=`bandwidth`+`p_filesize`,
|
||||
`hits`=`hits`+1,
|
||||
`unique_listeners`=`unique_listeners`+`p_new_listener`;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_hour`(`podcast_id`, `date`, `hour`)
|
||||
VALUES (p_podcast_id, @current_date, @current_hour)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`)
|
||||
VALUES (p_podcast_id, p_episode_id, @current_date, p_age)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_country`(`podcast_id`, `country_code`, `date`)
|
||||
VALUES (p_podcast_id, p_country_code, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_region`(`podcast_id`, `country_code`, `region_code`, `latitude`, `longitude`, `date`)
|
||||
VALUES (p_podcast_id, p_country_code, p_region_code, p_latitude, p_longitude, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
END IF;
|
||||
INSERT INTO `{$prefix}analytics_podcasts_by_player`(`podcast_id`, `service`, `app`, `device`, `os`, `is_bot`, `date`)
|
||||
VALUES (p_podcast_id, p_service, p_app, p_device, p_os, p_bot, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
END
|
||||
EOD;
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ EOD;
|
|||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
$this->db->query(
|
||||
"DROP PROCEDURE IF EXISTS `{$prefix}analytics_podcasts`"
|
||||
"DROP PROCEDURE IF EXISTS `{$prefix}analytics_podcasts`",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsUnknownUseragentsProcedure
|
||||
* Creates analytics_unknown_useragents procedure in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -20,14 +21,14 @@ class AddAnalyticsUnknownUseragentsProcedure extends Migration
|
|||
// Example: CALL analytics_unknown_useragents('Podcasts/1430.46 CFNetwork/1125.2 Darwin/19.4.0');
|
||||
$procedureName = $this->db->prefixTable('analytics_unknown_useragents');
|
||||
$createQuery = <<<EOD
|
||||
CREATE PROCEDURE `$procedureName` (IN `p_useragent` VARCHAR(191) CHARSET utf8mb4) MODIFIES SQL DATA
|
||||
DETERMINISTIC
|
||||
SQL SECURITY INVOKER
|
||||
COMMENT 'Add an unknown useragent to table $procedureName.'
|
||||
INSERT INTO `$procedureName`(`useragent`)
|
||||
VALUES (p_useragent)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1
|
||||
EOD;
|
||||
CREATE PROCEDURE `$procedureName` (IN `p_useragent` VARCHAR(191) CHARSET utf8mb4) MODIFIES SQL DATA
|
||||
DETERMINISTIC
|
||||
SQL SECURITY INVOKER
|
||||
COMMENT 'Add an unknown useragent to table $procedureName.'
|
||||
INSERT INTO `$procedureName`(`useragent`)
|
||||
VALUES (p_useragent)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1
|
||||
EOD;
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
* Class AddAnalyticsWebsiteProcedure
|
||||
* Creates analytics_website stored procedure in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
|
|
@ -20,25 +21,25 @@ class AddAnalyticsWebsiteProcedure extends Migration
|
|||
// Example: CALL analytics_website(1,'FR','Firefox');
|
||||
$procedureName = $this->db->prefixTable('analytics_website');
|
||||
$createQuery = <<<EOD
|
||||
CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` INT UNSIGNED, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_entry_page` VARCHAR(512) CHARSET utf8mb4, IN `p_referer_url` VARCHAR(512) CHARSET utf8mb4, IN `p_domain` VARCHAR(128) CHARSET utf8mb4, IN `p_keywords` VARCHAR(384) CHARSET utf8mb4) MODIFIES SQL DATA
|
||||
DETERMINISTIC
|
||||
SQL SECURITY INVOKER
|
||||
COMMENT 'Add one hit in website logs tables.'
|
||||
BEGIN
|
||||
CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` INT UNSIGNED, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_entry_page` VARCHAR(512) CHARSET utf8mb4, IN `p_referer_url` VARCHAR(512) CHARSET utf8mb4, IN `p_domain` VARCHAR(128) CHARSET utf8mb4, IN `p_keywords` VARCHAR(384) CHARSET utf8mb4) MODIFIES SQL DATA
|
||||
DETERMINISTIC
|
||||
SQL SECURITY INVOKER
|
||||
COMMENT 'Add one hit in website logs tables.'
|
||||
BEGIN
|
||||
|
||||
SET @current_date = DATE(NOW());
|
||||
SET @current_date = DATE(NOW());
|
||||
|
||||
INSERT INTO {$procedureName}_by_browser(`podcast_id`, `browser`, `date`)
|
||||
VALUES (p_podcast_id, p_browser, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer_url`, `domain`, `keywords`, `date`)
|
||||
VALUES (p_podcast_id, p_referer_url, p_domain, p_keywords, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO {$procedureName}_by_entry_page(`podcast_id`, `entry_page_url`, `date`)
|
||||
VALUES (p_podcast_id, p_entry_page, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
END
|
||||
EOD;
|
||||
INSERT INTO {$procedureName}_by_browser(`podcast_id`, `browser`, `date`)
|
||||
VALUES (p_podcast_id, p_browser, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer_url`, `domain`, `keywords`, `date`)
|
||||
VALUES (p_podcast_id, p_referer_url, p_domain, p_keywords, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
INSERT INTO {$procedureName}_by_entry_page(`podcast_id`, `entry_page_url`, `date`)
|
||||
VALUES (p_podcast_id, p_entry_page, @current_date)
|
||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||
END
|
||||
EOD;
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddLanguages
|
||||
* Creates languages table in database
|
||||
* Class AddPodcastUsers
|
||||
* Creates podcast_users table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
|
|
@ -32,9 +32,21 @@ class AddPodcastsUsers extends Migration
|
|||
],
|
||||
]);
|
||||
$this->forge->addPrimaryKey(['user_id', 'podcast_id']);
|
||||
$this->forge->addForeignKey('user_id', 'users', 'id');
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey('group_id', 'auth_groups', 'id');
|
||||
$this->forge->addForeignKey('user_id', 'users', 'id', false, 'CASCADE');
|
||||
$this->forge->addForeignKey(
|
||||
'podcast_id',
|
||||
'podcasts',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'group_id',
|
||||
'auth_groups',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('podcasts_users');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddLanguages
|
||||
* Creates languages table in database
|
||||
* Class AddPages
|
||||
* Creates pages table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
|
|
@ -46,7 +46,7 @@ class AddPages extends Migration
|
|||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->createTable('pages');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,20 @@ class AddPodcastsCategories extends Migration
|
|||
],
|
||||
]);
|
||||
$this->forge->addPrimaryKey(['podcast_id', 'category_id']);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey('category_id', 'categories', 'id');
|
||||
$this->forge->addForeignKey(
|
||||
'podcast_id',
|
||||
'podcasts',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'category_id',
|
||||
'categories',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('podcasts_categories');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@ class AddPersons extends Migration
|
|||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
|
||||
'image_mimetype' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 13,
|
||||
],
|
||||
'created_by' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
|
|
|
|||
|
|
@ -47,8 +47,20 @@ class AddPodcastsPersons extends Migration
|
|||
'person_group',
|
||||
'person_role',
|
||||
]);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey('person_id', 'persons', 'id');
|
||||
$this->forge->addForeignKey(
|
||||
'podcast_id',
|
||||
'podcasts',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'person_id',
|
||||
'persons',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('podcasts_persons');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,9 +52,27 @@ class AddEpisodesPersons extends Migration
|
|||
'person_group',
|
||||
'person_role',
|
||||
]);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->addForeignKey('episode_id', 'episodes', 'id');
|
||||
$this->forge->addForeignKey('person_id', 'persons', 'id');
|
||||
$this->forge->addForeignKey(
|
||||
'podcast_id',
|
||||
'podcasts',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'episode_id',
|
||||
'episodes',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'person_id',
|
||||
'persons',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('episodes_persons');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,16 +22,16 @@ class AddCreditView extends Migration
|
|||
$podcastPersonTable = $this->db->prefixTable('podcasts_persons');
|
||||
$episodePersonTable = $this->db->prefixTable('episodes_persons');
|
||||
$createQuery = <<<EOD
|
||||
CREATE VIEW `$viewName` AS
|
||||
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `$podcastPersonTable`
|
||||
INNER JOIN `$personTable`
|
||||
ON (`person_id`=`$personTable`.`id`)
|
||||
UNION
|
||||
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, `episode_id` FROM `$episodePersonTable`
|
||||
INNER JOIN `$personTable`
|
||||
ON (`person_id`=`$personTable`.`id`)
|
||||
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
|
||||
EOD;
|
||||
CREATE VIEW `$viewName` AS
|
||||
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `$podcastPersonTable`
|
||||
INNER JOIN `$personTable`
|
||||
ON (`person_id`=`$personTable`.`id`)
|
||||
UNION
|
||||
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, `episode_id` FROM `$episodePersonTable`
|
||||
INNER JOIN `$personTable`
|
||||
ON (`person_id`=`$personTable`.`id`)
|
||||
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
|
||||
EOD;
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddEpisodeIdToNotes
|
||||
* Adds episode_id field to activitypub_notes table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddEpisodeIdToNotes extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
||||
$createQuery = <<<SQL
|
||||
ALTER TABLE ${prefix}activitypub_notes
|
||||
ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`,
|
||||
ADD FOREIGN KEY ${prefix}activitypub_notes_episode_id_foreign(episode_id) REFERENCES ${prefix}episodes(id) ON DELETE CASCADE;
|
||||
SQL;
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropForeignKey(
|
||||
'activitypub_notes',
|
||||
'activitypub_notes_episode_id_foreign',
|
||||
);
|
||||
$this->forge->dropColumn('activitypub_notes', 'episode_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddCreatedByToNotes
|
||||
* Adds created_by field to activitypub_notes table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddCreatedByToNotes extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
||||
$createQuery = <<<SQL
|
||||
ALTER TABLE ${prefix}activitypub_notes
|
||||
ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`,
|
||||
ADD FOREIGN KEY ${prefix}activitypub_notes_created_by_foreign(created_by) REFERENCES ${prefix}users(id) ON DELETE CASCADE;
|
||||
SQL;
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropForeignKey(
|
||||
'activitypub_notes',
|
||||
'activitypub_notes_created_by_foreign',
|
||||
);
|
||||
$this->forge->dropColumn('activitypub_notes', 'created_by');
|
||||
}
|
||||
}
|
||||
|
|
@ -158,6 +158,18 @@ class AuthSeeder extends Seeder
|
|||
'description' => 'Set / remove platform links of a podcast',
|
||||
'has_permission' => ['podcast_admin'],
|
||||
],
|
||||
[
|
||||
'name' => 'manage_publications',
|
||||
'description' =>
|
||||
'Publish / unpublish episodes & notes of a podcast',
|
||||
'has_permission' => ['podcast_admin'],
|
||||
],
|
||||
[
|
||||
'name' => 'interact_as',
|
||||
'description' =>
|
||||
'Interact as the podcast to favourite / share or reply to notes.',
|
||||
'has_permission' => ['podcast_admin'],
|
||||
],
|
||||
],
|
||||
'podcast_episodes' => [
|
||||
[
|
||||
|
|
@ -192,11 +204,6 @@ class AuthSeeder extends Seeder
|
|||
'Delete all occurrences of an episode of a podcast from the database',
|
||||
'has_permission' => ['podcast_admin'],
|
||||
],
|
||||
[
|
||||
'name' => 'manage_publications',
|
||||
'description' => 'Publish / unpublish episodes of a podcast',
|
||||
'has_permission' => ['podcast_admin'],
|
||||
],
|
||||
],
|
||||
'person' => [
|
||||
[
|
||||
|
|
@ -220,8 +227,23 @@ class AuthSeeder extends Seeder
|
|||
'has_permission' => ['superadmin'],
|
||||
],
|
||||
[
|
||||
'name' => 'delete_permanently',
|
||||
'description' => 'Delete any person from the database',
|
||||
'name' => 'delete',
|
||||
'description' =>
|
||||
'Delete permanently any person from the database',
|
||||
'has_permission' => ['superadmin'],
|
||||
],
|
||||
],
|
||||
'fediverse' => [
|
||||
[
|
||||
'name' => 'block_actors',
|
||||
'description' =>
|
||||
'Block an activitypub actors from interacting with the instance.',
|
||||
'has_permission' => ['superadmin'],
|
||||
],
|
||||
[
|
||||
'name' => 'block_domains',
|
||||
'description' =>
|
||||
'Block an activitypub domains from interacting with the instance.',
|
||||
'has_permission' => ['superadmin'],
|
||||
],
|
||||
],
|
||||
|
|
@ -266,7 +288,7 @@ class AuthSeeder extends Seeder
|
|||
array_push($dataGroupsPermissions, [
|
||||
'group_id' => $this->getGroupIdByName(
|
||||
$role,
|
||||
$dataGroups
|
||||
$dataGroups,
|
||||
),
|
||||
'permission_id' => $permissionId,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ class PlatformSeeder extends Seeder
|
|||
'home_url' => 'https://fyyd.de/',
|
||||
'submit_url' => 'https://fyyd.de/add-feed',
|
||||
],
|
||||
|
||||
[
|
||||
'slug' => 'google',
|
||||
'type' => 'podcasting',
|
||||
|
|
@ -249,7 +248,6 @@ class PlatformSeeder extends Seeder
|
|||
'submit_url' =>
|
||||
'https://help.tunein.com/contact/add-podcast-S19TR3Sdf',
|
||||
],
|
||||
|
||||
[
|
||||
'slug' => 'paypal',
|
||||
'type' => 'funding',
|
||||
|
|
@ -257,7 +255,6 @@ class PlatformSeeder extends Seeder
|
|||
'home_url' => 'https://www.paypal.com/',
|
||||
'submit_url' => 'https://www.paypal.com/paypalme/my/grab',
|
||||
],
|
||||
|
||||
[
|
||||
'slug' => 'gofundme',
|
||||
'type' => 'funding',
|
||||
|
|
@ -322,7 +319,6 @@ class PlatformSeeder extends Seeder
|
|||
'home_url' => 'https://www.ulule.com/',
|
||||
'submit_url' => 'https://www.ulule.com/projects/create/#/',
|
||||
],
|
||||
|
||||
[
|
||||
'slug' => 'discord',
|
||||
'type' => 'social',
|
||||
|
|
@ -431,6 +427,7 @@ class PlatformSeeder extends Seeder
|
|||
'submit_url' => 'https://creatoracademy.youtube.com/page/home',
|
||||
],
|
||||
];
|
||||
|
||||
$this->db
|
||||
->table('platforms')
|
||||
->ignore(true)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class Credit extends Entity
|
|||
protected $podcast;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Episode
|
||||
* @var \App\Entities\Episode|null
|
||||
*/
|
||||
protected $episode;
|
||||
|
||||
|
|
@ -44,50 +44,61 @@ class Credit extends Entity
|
|||
public function getPodcast()
|
||||
{
|
||||
return (new PodcastModel())->getPodcastById(
|
||||
$this->attributes['podcast_id']
|
||||
$this->attributes['podcast_id'],
|
||||
);
|
||||
}
|
||||
|
||||
public function getEpisode()
|
||||
{
|
||||
if (empty($this->attributes['episode_id'])) {
|
||||
return null;
|
||||
} else {
|
||||
return (new EpisodeModel())->getEpisodeById(
|
||||
$this->attributes['podcast_id'],
|
||||
$this->attributes['episode_id']
|
||||
if (empty($this->episode_id)) {
|
||||
throw new \RuntimeException(
|
||||
'Credit must have episode_id before getting episode.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->episode)) {
|
||||
$this->episode = (new EpisodeModel())->getPublishedEpisodeById(
|
||||
$this->episode_id,
|
||||
$this->podcast_id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->episode;
|
||||
}
|
||||
|
||||
public function getPerson()
|
||||
{
|
||||
return (new PersonModel())->getPersonById(
|
||||
$this->attributes['person_id']
|
||||
);
|
||||
if (empty($this->person_id)) {
|
||||
throw new \RuntimeException(
|
||||
'Credit must have person_id before getting person.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->person)) {
|
||||
$this->person = (new PersonModel())->getPersonById(
|
||||
$this->person_id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->person;
|
||||
}
|
||||
|
||||
public function getGroupLabel()
|
||||
{
|
||||
if (empty($this->attributes['person_group'])) {
|
||||
if (empty($this->person_group)) {
|
||||
return null;
|
||||
} else {
|
||||
return lang(
|
||||
"PersonsTaxonomy.persons.{$this->attributes['person_group']}.label"
|
||||
);
|
||||
return lang("PersonsTaxonomy.persons.{$this->person_group}.label");
|
||||
}
|
||||
}
|
||||
|
||||
public function getRoleLabel()
|
||||
{
|
||||
if (
|
||||
empty($this->attributes['person_group']) ||
|
||||
empty($this->attributes['person_role'])
|
||||
) {
|
||||
if (empty($this->person_group) || empty($this->person_role)) {
|
||||
return null;
|
||||
} else {
|
||||
return lang(
|
||||
"PersonsTaxonomy.persons.{$this->attributes['person_group']}.roles.{$this->attributes['person_role']}.label"
|
||||
"PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace App\Entities;
|
|||
use App\Models\PodcastModel;
|
||||
use App\Models\SoundbiteModel;
|
||||
use App\Models\EpisodePersonModel;
|
||||
use App\Models\NoteModel;
|
||||
use CodeIgniter\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
|
@ -28,7 +29,7 @@ class Episode extends Entity
|
|||
protected $link;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Image
|
||||
* @var \App\Libraries\Image
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
|
|
@ -80,13 +81,18 @@ class Episode extends Entity
|
|||
/**
|
||||
* @var \App\Entities\EpisodePerson[]
|
||||
*/
|
||||
protected $episode_persons;
|
||||
protected $persons;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Soundbite[]
|
||||
*/
|
||||
protected $soundbites;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Note[]
|
||||
*/
|
||||
protected $notes;
|
||||
|
||||
/**
|
||||
* Holds text only description, striped of any markdown or html special characters
|
||||
*
|
||||
|
|
@ -122,6 +128,7 @@ class Episode extends Entity
|
|||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'podcast_id' => 'integer',
|
||||
'guid' => 'string',
|
||||
'slug' => 'string',
|
||||
'title' => 'string',
|
||||
|
|
@ -133,6 +140,7 @@ class Episode extends Entity
|
|||
'description_markdown' => 'string',
|
||||
'description_html' => 'string',
|
||||
'image_uri' => '?string',
|
||||
'image_mimetype' => '?string',
|
||||
'transcript_uri' => '?string',
|
||||
'chapters_uri' => '?string',
|
||||
'parental_advisory' => '?string',
|
||||
|
|
@ -144,6 +152,9 @@ class Episode extends Entity
|
|||
'location_geo' => '?string',
|
||||
'location_osmid' => '?string',
|
||||
'custom_rss' => '?json-array',
|
||||
'favourites_total' => 'integer',
|
||||
'reblogs_total' => 'integer',
|
||||
'notes_total' => 'integer',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
|
@ -163,15 +174,16 @@ class Episode extends Entity
|
|||
) {
|
||||
helper('media');
|
||||
|
||||
// check whether the user has inputted an image and store it
|
||||
$this->attributes['image_uri'] = save_podcast_media(
|
||||
// check whether the user has inputted an image and store
|
||||
$this->attributes['image_mimetype'] = $image->getMimeType();
|
||||
$this->attributes['image_uri'] = save_media(
|
||||
$image,
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
'podcasts/' . $this->getPodcast()->name,
|
||||
$this->attributes['slug'],
|
||||
);
|
||||
|
||||
$this->image = new \App\Entities\Image(
|
||||
$this->attributes['image_uri']
|
||||
$this->image = new \App\Libraries\Image(
|
||||
$this->attributes['image_uri'],
|
||||
$this->attributes['image_mimetype'],
|
||||
);
|
||||
$this->image->saveSizes();
|
||||
}
|
||||
|
|
@ -179,10 +191,13 @@ class Episode extends Entity
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getImage(): \App\Entities\Image
|
||||
public function getImage(): \App\Libraries\Image
|
||||
{
|
||||
if ($image_uri = $this->attributes['image_uri']) {
|
||||
return new \App\Entities\Image($image_uri);
|
||||
return new \App\Libraries\Image(
|
||||
$image_uri,
|
||||
$this->attributes['image_mimetype'],
|
||||
);
|
||||
}
|
||||
return $this->getPodcast()->image;
|
||||
}
|
||||
|
|
@ -204,13 +219,13 @@ class Episode extends Entity
|
|||
|
||||
$enclosure_metadata = get_file_tags($enclosure);
|
||||
|
||||
$this->attributes['enclosure_uri'] = save_podcast_media(
|
||||
$this->attributes['enclosure_uri'] = save_media(
|
||||
$enclosure,
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
'podcasts/' . $this->getPodcast()->name,
|
||||
$this->attributes['slug'],
|
||||
);
|
||||
$this->attributes['enclosure_duration'] = round(
|
||||
$enclosure_metadata['playtime_seconds']
|
||||
$enclosure_metadata['playtime_seconds'],
|
||||
);
|
||||
$this->attributes['enclosure_mimetype'] =
|
||||
$enclosure_metadata['mime_type'];
|
||||
|
|
@ -238,10 +253,10 @@ class Episode extends Entity
|
|||
) {
|
||||
helper('media');
|
||||
|
||||
$this->attributes['transcript_uri'] = save_podcast_media(
|
||||
$this->attributes['transcript_uri'] = save_media(
|
||||
$transcript,
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug'] . '-transcript'
|
||||
$this->attributes['slug'] . '-transcript',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -263,10 +278,10 @@ class Episode extends Entity
|
|||
) {
|
||||
helper('media');
|
||||
|
||||
$this->attributes['chapters_uri'] = save_podcast_media(
|
||||
$this->attributes['chapters_uri'] = save_media(
|
||||
$chapters,
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug'] . '-chapters'
|
||||
$this->attributes['slug'] . '-chapters',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -343,15 +358,15 @@ class Episode extends Entity
|
|||
$this->attributes[
|
||||
'enclosure_duration'
|
||||
]) *
|
||||
60
|
||||
60,
|
||||
),
|
||||
$this->attributes['enclosure_filesize'],
|
||||
$this->attributes['enclosure_duration'],
|
||||
strtotime($this->attributes['published_at'])
|
||||
)
|
||||
strtotime($this->attributes['published_at']),
|
||||
),
|
||||
),
|
||||
$this->attributes['enclosure_uri']
|
||||
)
|
||||
$this->attributes['enclosure_uri'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -384,22 +399,22 @@ class Episode extends Entity
|
|||
*
|
||||
* @return \App\Entities\EpisodePerson[]
|
||||
*/
|
||||
public function getEpisodePersons()
|
||||
public function getPersons()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Episode must be created before getting persons.'
|
||||
'Episode must be created before getting persons.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->episode_persons)) {
|
||||
$this->episode_persons = (new EpisodePersonModel())->getPersonsByEpisodeId(
|
||||
if (empty($this->persons)) {
|
||||
$this->persons = (new EpisodePersonModel())->getPersonsByEpisodeId(
|
||||
$this->podcast_id,
|
||||
$this->id
|
||||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->episode_persons;
|
||||
return $this->persons;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -411,28 +426,43 @@ class Episode extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Episode must be created before getting soundbites.'
|
||||
'Episode must be created before getting soundbites.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->soundbites)) {
|
||||
$this->soundbites = (new SoundbiteModel())->getEpisodeSoundbites(
|
||||
$this->getPodcast()->id,
|
||||
$this->id
|
||||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->soundbites;
|
||||
}
|
||||
|
||||
public function getNotes()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Episode must be created before getting soundbites.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->notes)) {
|
||||
$this->notes = (new NoteModel())->getEpisodeNotes($this->id);
|
||||
}
|
||||
|
||||
return $this->notes;
|
||||
}
|
||||
|
||||
public function getLink()
|
||||
{
|
||||
return base_url(
|
||||
route_to(
|
||||
'episode',
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
)
|
||||
$this->attributes['slug'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -444,13 +474,13 @@ class Episode extends Entity
|
|||
'embeddable-player-theme',
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug'],
|
||||
$theme
|
||||
$theme,
|
||||
)
|
||||
: route_to(
|
||||
'embeddable-player',
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
)
|
||||
$this->attributes['slug'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -464,7 +494,7 @@ class Episode extends Entity
|
|||
public function getPodcast()
|
||||
{
|
||||
return (new PodcastModel())->getPodcastById(
|
||||
$this->attributes['podcast_id']
|
||||
$this->attributes['podcast_id'],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +507,7 @@ class Episode extends Entity
|
|||
|
||||
$this->attributes['description_markdown'] = $descriptionMarkdown;
|
||||
$this->attributes['description_html'] = $converter->convertToHtml(
|
||||
$descriptionMarkdown
|
||||
$descriptionMarkdown,
|
||||
);
|
||||
|
||||
return $this;
|
||||
|
|
@ -510,25 +540,11 @@ class Episode extends Entity
|
|||
preg_replace(
|
||||
'/\s+/',
|
||||
' ',
|
||||
strip_tags($this->attributes['description_html'])
|
||||
)
|
||||
strip_tags($this->attributes['description_html']),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function setCreatedBy(\App\Entities\User $user)
|
||||
{
|
||||
$this->attributes['created_by'] = $user->id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(\App\Entities\User $user)
|
||||
{
|
||||
$this->attributes['updated_by'] = $user->id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPublicationStatus()
|
||||
{
|
||||
if ($this->publication_status) {
|
||||
|
|
@ -588,7 +604,7 @@ class Episode extends Entity
|
|||
return '';
|
||||
} else {
|
||||
$xmlNode = (new \App\Libraries\SimpleRSSElement(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>'
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
|
||||
))
|
||||
->addChild('channel')
|
||||
->addChild('item');
|
||||
|
|
@ -596,7 +612,7 @@ class Episode extends Entity
|
|||
[
|
||||
'elements' => $this->custom_rss,
|
||||
],
|
||||
$xmlNode
|
||||
$xmlNode,
|
||||
);
|
||||
return str_replace(['<item>', '</item>'], '', $xmlNode->asXML());
|
||||
}
|
||||
|
|
@ -615,12 +631,12 @@ class Episode extends Entity
|
|||
simplexml_load_string(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><item>' .
|
||||
$customRssString .
|
||||
'</item></channel></rss>'
|
||||
)
|
||||
'</item></channel></rss>',
|
||||
),
|
||||
)['elements'][0]['elements'][0];
|
||||
if (array_key_exists('elements', $customRssArray)) {
|
||||
$this->attributes['custom_rss'] = json_encode(
|
||||
$customRssArray['elements']
|
||||
$customRssArray['elements'],
|
||||
);
|
||||
} else {
|
||||
$this->attributes['custom_rss'] = null;
|
||||
|
|
|
|||
|
|
@ -1,151 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
use CodeIgniter\Entity;
|
||||
|
||||
class Image extends Entity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $original_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $original_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $thumbnail_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $thumbnail_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $medium_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $medium_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $large_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $large_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $feed_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $feed_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id3_path;
|
||||
|
||||
public function __construct($originalUri)
|
||||
{
|
||||
helper('media');
|
||||
|
||||
$originalPath = media_path($originalUri);
|
||||
|
||||
[
|
||||
'filename' => $filename,
|
||||
'dirname' => $dirname,
|
||||
'extension' => $extension,
|
||||
] = pathinfo($originalPath);
|
||||
|
||||
// load images extensions from config
|
||||
$imageConfig = config('Images');
|
||||
$thumbnailExtension = $imageConfig->thumbnailExtension;
|
||||
$mediumExtension = $imageConfig->mediumExtension;
|
||||
$largeExtension = $imageConfig->largeExtension;
|
||||
$feedExtension = $imageConfig->feedExtension;
|
||||
$id3Extension = $imageConfig->id3Extension;
|
||||
|
||||
$thumbnail =
|
||||
$dirname . '/' . $filename . $thumbnailExtension . '.' . $extension;
|
||||
$medium =
|
||||
$dirname . '/' . $filename . $mediumExtension . '.' . $extension;
|
||||
$large =
|
||||
$dirname . '/' . $filename . $largeExtension . '.' . $extension;
|
||||
$feed = $dirname . '/' . $filename . $feedExtension . '.' . $extension;
|
||||
$id3 = $dirname . '/' . $filename . $id3Extension . '.' . $extension;
|
||||
|
||||
parent::__construct([
|
||||
'original_path' => $originalPath,
|
||||
'original_url' => media_url($originalUri),
|
||||
'thumbnail_path' => $thumbnail,
|
||||
'thumbnail_url' => base_url($thumbnail),
|
||||
'medium_path' => $medium,
|
||||
'medium_url' => base_url($medium),
|
||||
'large_path' => $large,
|
||||
'large_url' => base_url($large),
|
||||
'feed_path' => $feed,
|
||||
'feed_url' => base_url($feed),
|
||||
'id3_path' => $id3,
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveSizes()
|
||||
{
|
||||
// load images sizes from config
|
||||
$imageConfig = config('Images');
|
||||
$thumbnailSize = $imageConfig->thumbnailSize;
|
||||
$mediumSize = $imageConfig->mediumSize;
|
||||
$largeSize = $imageConfig->largeSize;
|
||||
$feedSize = $imageConfig->feedSize;
|
||||
$id3Size = $imageConfig->id3Size;
|
||||
|
||||
$imageService = \Config\Services::image();
|
||||
|
||||
$imageService
|
||||
->withFile($this->attributes['original_path'])
|
||||
->resize($thumbnailSize, $thumbnailSize)
|
||||
->save($this->attributes['thumbnail_path']);
|
||||
|
||||
$imageService
|
||||
->withFile($this->attributes['original_path'])
|
||||
->resize($mediumSize, $mediumSize)
|
||||
->save($this->attributes['medium_path']);
|
||||
|
||||
$imageService
|
||||
->withFile($this->attributes['original_path'])
|
||||
->resize($largeSize, $largeSize)
|
||||
->save($this->attributes['large_path']);
|
||||
|
||||
$imageService
|
||||
->withFile($this->attributes['original_path'])
|
||||
->resize($feedSize, $feedSize)
|
||||
->save($this->attributes['feed_path']);
|
||||
|
||||
$imageService
|
||||
->withFile($this->attributes['original_path'])
|
||||
->resize($id3Size, $id3Size)
|
||||
->save($this->attributes['id3_path']);
|
||||
}
|
||||
}
|
||||
56
app/Entities/Note.php
Normal file
56
app/Entities/Note.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
use App\Models\EpisodeModel;
|
||||
|
||||
class Note extends \ActivityPub\Entities\Note
|
||||
{
|
||||
/**
|
||||
* @var \App\Entities\Episode|null
|
||||
*/
|
||||
protected $episode;
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'string',
|
||||
'uri' => 'string',
|
||||
'actor_id' => 'integer',
|
||||
'in_reply_to_id' => '?string',
|
||||
'reblog_of_id' => '?string',
|
||||
'episode_id' => '?integer',
|
||||
'message' => 'string',
|
||||
'message_html' => 'string',
|
||||
'favourites_count' => 'integer',
|
||||
'reblogs_count' => 'integer',
|
||||
'replies_count' => 'integer',
|
||||
'created_by' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the note's attached episode
|
||||
*
|
||||
* @return \App\Entities\Episode
|
||||
*/
|
||||
public function getEpisode()
|
||||
{
|
||||
if (empty($this->episode_id)) {
|
||||
throw new \RuntimeException(
|
||||
'Note must have an episode_id before getting episode.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->episode)) {
|
||||
$this->episode = (new EpisodeModel())->getEpisodeById(
|
||||
$this->episode_id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->episode;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ use CodeIgniter\Entity;
|
|||
class Person extends Entity
|
||||
{
|
||||
/**
|
||||
* @var \App\Entities\Image
|
||||
* @var \App\Libraries\Image
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
|
|
@ -23,12 +23,13 @@ class Person extends Entity
|
|||
'unique_name' => 'string',
|
||||
'information_url' => '?string',
|
||||
'image_uri' => 'string',
|
||||
'image_mimetype' => 'string',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Saves a picture in `public/media/~person/`
|
||||
* Saves a picture in `public/media/persons/`
|
||||
*
|
||||
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image
|
||||
*
|
||||
|
|
@ -38,13 +39,15 @@ class Person extends Entity
|
|||
if ($image) {
|
||||
helper('media');
|
||||
|
||||
$this->attributes['image_uri'] = save_podcast_media(
|
||||
$this->attributes['image_mimetype'] = $image->getMimeType();
|
||||
$this->attributes['image_uri'] = save_media(
|
||||
$image,
|
||||
'~person',
|
||||
$this->attributes['unique_name']
|
||||
'persons',
|
||||
$this->attributes['unique_name'],
|
||||
);
|
||||
$this->image = new \App\Entities\Image(
|
||||
$this->attributes['image_uri']
|
||||
$this->image = new \App\Libraries\Image(
|
||||
$this->attributes['image_uri'],
|
||||
$this->attributes['image_mimetype'],
|
||||
);
|
||||
$this->image->saveSizes();
|
||||
}
|
||||
|
|
@ -54,6 +57,9 @@ class Person extends Entity
|
|||
|
||||
public function getImage()
|
||||
{
|
||||
return new \App\Entities\Image($this->attributes['image_uri']);
|
||||
return new \App\Libraries\Image(
|
||||
$this->attributes['image_uri'],
|
||||
$this->attributes['image_mimetype'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
namespace App\Entities;
|
||||
|
||||
use ActivityPub\Models\ActorModel;
|
||||
use App\Models\CategoryModel;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PlatformModel;
|
||||
|
|
@ -24,7 +25,12 @@ class Podcast extends Entity
|
|||
protected $link;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Image
|
||||
* @var \ActivityPub\Entities\Actor
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* @var \App\Libraries\Image
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
|
|
@ -36,7 +42,7 @@ class Podcast extends Entity
|
|||
/**
|
||||
* @var \App\Entities\PodcastPerson[]
|
||||
*/
|
||||
protected $podcast_persons;
|
||||
protected $persons;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Category
|
||||
|
|
@ -89,11 +95,13 @@ class Podcast extends Entity
|
|||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'title' => 'string',
|
||||
'actor_id' => 'integer',
|
||||
'name' => 'string',
|
||||
'title' => 'string',
|
||||
'description_markdown' => 'string',
|
||||
'description_html' => 'string',
|
||||
'image_uri' => 'string',
|
||||
'image_mimetype' => 'string',
|
||||
'language_code' => 'string',
|
||||
'category_id' => 'integer',
|
||||
'parental_advisory' => '?string',
|
||||
|
|
@ -121,6 +129,26 @@ class Podcast extends Entity
|
|||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the podcast actor
|
||||
*
|
||||
* @return \App\Entities\Actor
|
||||
*/
|
||||
public function getActor()
|
||||
{
|
||||
if (!$this->attributes['actor_id']) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must have an actor_id before getting actor.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->actor)) {
|
||||
$this->actor = (new ActorModel())->getActorById($this->actor_id);
|
||||
}
|
||||
|
||||
return $this->actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a cover image to the corresponding podcast folder in `public/media/podcast_name/`
|
||||
*
|
||||
|
|
@ -132,13 +160,16 @@ class Podcast extends Entity
|
|||
if ($image) {
|
||||
helper('media');
|
||||
|
||||
$this->attributes['image_uri'] = save_podcast_media(
|
||||
$this->attributes['image_mimetype'] = $image->getMimeType();
|
||||
$this->attributes['image_uri'] = save_media(
|
||||
$image,
|
||||
$this->attributes['name'],
|
||||
'cover'
|
||||
'podcasts/' . $this->attributes['name'],
|
||||
'cover',
|
||||
);
|
||||
$this->image = new \App\Entities\Image(
|
||||
$this->attributes['image_uri']
|
||||
|
||||
$this->image = new \App\Libraries\Image(
|
||||
$this->attributes['image_uri'],
|
||||
$this->attributes['image_mimetype'],
|
||||
);
|
||||
$this->image->saveSizes();
|
||||
}
|
||||
|
|
@ -148,17 +179,20 @@ class Podcast extends Entity
|
|||
|
||||
public function getImage()
|
||||
{
|
||||
return new \App\Entities\Image($this->attributes['image_uri']);
|
||||
return new \App\Libraries\Image(
|
||||
$this->attributes['image_uri'],
|
||||
$this->attributes['image_mimetype'],
|
||||
);
|
||||
}
|
||||
|
||||
public function getLink()
|
||||
{
|
||||
return base_url(route_to('podcast', $this->attributes['name']));
|
||||
return url_to('podcast-activity', $this->attributes['name']);
|
||||
}
|
||||
|
||||
public function getFeedUrl()
|
||||
{
|
||||
return base_url(route_to('podcast_feed', $this->attributes['name']));
|
||||
return url_to('podcast_feed', $this->attributes['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -170,14 +204,14 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting episodes.'
|
||||
'Podcast must be created before getting episodes.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->episodes)) {
|
||||
$this->episodes = (new EpisodeModel())->getPodcastEpisodes(
|
||||
$this->id,
|
||||
$this->type
|
||||
$this->type,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -189,21 +223,21 @@ class Podcast extends Entity
|
|||
*
|
||||
* @return \App\Entities\PodcastPerson[]
|
||||
*/
|
||||
public function getPodcastPersons()
|
||||
public function getPersons()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting persons.'
|
||||
'Podcast must be created before getting persons.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->podcast_persons)) {
|
||||
$this->podcast_persons = (new PodcastPersonModel())->getPersonsByPodcastId(
|
||||
$this->id
|
||||
if (empty($this->persons)) {
|
||||
$this->persons = (new PodcastPersonModel())->getPersonsByPodcastId(
|
||||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->podcast_persons;
|
||||
return $this->persons;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -215,7 +249,7 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting category.'
|
||||
'Podcast must be created before getting category.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -235,13 +269,13 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcasts must be created before getting contributors.'
|
||||
'Podcasts must be created before getting contributors.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->contributors)) {
|
||||
$this->contributors = (new UserModel())->getPodcastContributors(
|
||||
$this->id
|
||||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +291,7 @@ class Podcast extends Entity
|
|||
|
||||
$this->attributes['description_markdown'] = $descriptionMarkdown;
|
||||
$this->attributes['description_html'] = $converter->convertToHtml(
|
||||
$descriptionMarkdown
|
||||
$descriptionMarkdown,
|
||||
);
|
||||
|
||||
return $this;
|
||||
|
|
@ -293,25 +327,11 @@ class Podcast extends Entity
|
|||
preg_replace(
|
||||
'/\s+/',
|
||||
' ',
|
||||
strip_tags($this->attributes['description_html'])
|
||||
)
|
||||
strip_tags($this->attributes['description_html']),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function setCreatedBy(\App\Entities\User $user)
|
||||
{
|
||||
$this->attributes['created_by'] = $user->id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(\App\Entities\User $user)
|
||||
{
|
||||
$this->attributes['updated_by'] = $user->id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the podcast's podcasting platform links
|
||||
*
|
||||
|
|
@ -321,14 +341,14 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting podcasting platform links.'
|
||||
'Podcast must be created before getting podcasting platform links.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->podcastingPlatforms)) {
|
||||
$this->podcastingPlatforms = (new PlatformModel())->getPodcastPlatforms(
|
||||
$this->id,
|
||||
'podcasting'
|
||||
'podcasting',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +362,7 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting podcasting platform.'
|
||||
'Podcast must be created before getting podcasting platform.',
|
||||
);
|
||||
}
|
||||
foreach ($this->getPodcastingPlatforms() as $podcastingPlatform) {
|
||||
|
|
@ -362,14 +382,14 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting social platform links.'
|
||||
'Podcast must be created before getting social platform links.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->socialPlatforms)) {
|
||||
$this->socialPlatforms = (new PlatformModel())->getPodcastPlatforms(
|
||||
$this->id,
|
||||
'social'
|
||||
'social',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -383,7 +403,7 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting social platform.'
|
||||
'Podcast must be created before getting social platform.',
|
||||
);
|
||||
}
|
||||
foreach ($this->getSocialPlatforms() as $socialPlatform) {
|
||||
|
|
@ -403,14 +423,14 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting funding platform links.'
|
||||
'Podcast must be created before getting funding platform links.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->fundingPlatforms)) {
|
||||
$this->fundingPlatforms = (new PlatformModel())->getPodcastPlatforms(
|
||||
$this->id,
|
||||
'funding'
|
||||
'funding',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -424,7 +444,7 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting Funding platform.'
|
||||
'Podcast must be created before getting Funding platform.',
|
||||
);
|
||||
}
|
||||
foreach ($this->getFundingPlatforms() as $fundingPlatform) {
|
||||
|
|
@ -439,13 +459,13 @@ class Podcast extends Entity
|
|||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting other categories.'
|
||||
'Podcast must be created before getting other categories.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->other_categories)) {
|
||||
$this->other_categories = (new CategoryModel())->getPodcastCategories(
|
||||
$this->id
|
||||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -457,7 +477,7 @@ class Podcast extends Entity
|
|||
if (empty($this->other_categories_ids)) {
|
||||
$this->other_categories_ids = array_column(
|
||||
$this->getOtherCategories(),
|
||||
'id'
|
||||
'id',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -505,18 +525,18 @@ class Podcast extends Entity
|
|||
return '';
|
||||
} else {
|
||||
$xmlNode = (new \App\Libraries\SimpleRSSElement(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>'
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
|
||||
))->addChild('channel');
|
||||
array_to_rss(
|
||||
[
|
||||
'elements' => $this->custom_rss,
|
||||
],
|
||||
$xmlNode
|
||||
$xmlNode,
|
||||
);
|
||||
return str_replace(
|
||||
['<channel>', '</channel>'],
|
||||
'',
|
||||
$xmlNode->asXML()
|
||||
$xmlNode->asXML(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -534,12 +554,12 @@ class Podcast extends Entity
|
|||
simplexml_load_string(
|
||||
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel>' .
|
||||
$customRssString .
|
||||
'</channel></rss>'
|
||||
)
|
||||
'</channel></rss>',
|
||||
),
|
||||
)['elements'][0];
|
||||
if (array_key_exists('elements', $customRssArray)) {
|
||||
$this->attributes['custom_rss'] = json_encode(
|
||||
$customRssArray['elements']
|
||||
$customRssArray['elements'],
|
||||
);
|
||||
} else {
|
||||
$this->attributes['custom_rss'] = null;
|
||||
|
|
|
|||
|
|
@ -23,13 +23,6 @@ class Soundbite extends Entity
|
|||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
public function setCreatedBy(\App\Entities\User $user)
|
||||
{
|
||||
$this->attributes['created_by'] = $user->id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(\App\Entities\User $user)
|
||||
{
|
||||
$this->attributes['updated_by'] = $user->id;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use CodeIgniter\HTTP\ResponseInterface;
|
|||
use CodeIgniter\Filters\FilterInterface;
|
||||
use Myth\Auth\Exceptions\PermissionException;
|
||||
|
||||
class Permission implements FilterInterface
|
||||
class PermissionFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* Do whatever processing this filter needs to do.
|
||||
|
|
@ -6,30 +6,6 @@
|
|||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
/**
|
||||
* For compatibility with PHP-FPM v7.2 and below:
|
||||
*/
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders()
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[
|
||||
str_replace(
|
||||
' ',
|
||||
'-',
|
||||
ucwords(
|
||||
strtolower(str_replace('_', ' ', substr($name, 5)))
|
||||
)
|
||||
)
|
||||
] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode Base64 for URLs
|
||||
*/
|
||||
|
|
@ -57,7 +33,7 @@ function set_user_session_deny_list_ip()
|
|||
if (!$session->has('denyListIp')) {
|
||||
$session->set(
|
||||
'denyListIp',
|
||||
\Podlibre\Ipcat\IpDb::find($_SERVER['REMOTE_ADDR']) != null
|
||||
\Podlibre\Ipcat\IpDb::find($_SERVER['REMOTE_ADDR']) != null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +57,7 @@ function set_user_session_location()
|
|||
if (!$session->has('location')) {
|
||||
try {
|
||||
$cityReader = new \GeoIp2\Database\Reader(
|
||||
WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb'
|
||||
WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb',
|
||||
);
|
||||
$city = $cityReader->city($_SERVER['REMOTE_ADDR']);
|
||||
|
||||
|
|
@ -132,7 +108,7 @@ function set_user_session_player()
|
|||
try {
|
||||
$db = \Config\Database::connect();
|
||||
$procedureNameAnalyticsUnknownUseragents = $db->prefixTable(
|
||||
'analytics_unknown_useragents'
|
||||
'analytics_unknown_useragents',
|
||||
);
|
||||
$db->query("CALL $procedureNameAnalyticsUnknownUseragents(?)", [
|
||||
$userAgent,
|
||||
|
|
@ -283,7 +259,7 @@ function podcast_hit(
|
|||
'_' .
|
||||
$_SERVER['HTTP_USER_AGENT'] .
|
||||
'_' .
|
||||
$episodeId
|
||||
$episodeId,
|
||||
);
|
||||
// Was this episode downloaded in the past 24h:
|
||||
$downloadedBytes = cache($episodeHashId);
|
||||
|
|
@ -335,7 +311,7 @@ function podcast_hit(
|
|||
'_' .
|
||||
$_SERVER['HTTP_USER_AGENT'] .
|
||||
'_' .
|
||||
$podcastId
|
||||
$podcastId,
|
||||
);
|
||||
$newListener = 1;
|
||||
// Has this listener already downloaded an episode today:
|
||||
|
|
@ -370,7 +346,7 @@ function podcast_hit(
|
|||
$duration,
|
||||
$age,
|
||||
$newListener,
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
89
app/Helpers/auth_helper.php
Normal file
89
app/Helpers/auth_helper.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
use ActivityPub\Models\ActorModel;
|
||||
use CodeIgniter\Database\Exceptions\DataException;
|
||||
use Config\Services;
|
||||
|
||||
if (!function_exists('set_interact_as_actor')) {
|
||||
/**
|
||||
* Sets the actor id of which the user is acting as
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function set_interact_as_actor($actorId)
|
||||
{
|
||||
$authenticate = Services::authentication();
|
||||
$authenticate->check();
|
||||
|
||||
$session = session();
|
||||
$session->set('interact_as_actor_id', $actorId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('remove_interact_as_actor')) {
|
||||
/**
|
||||
* Removes the actor id of which the user is acting as
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function remove_interact_as_actor()
|
||||
{
|
||||
$session = session();
|
||||
$session->remove('interact_as_actor_id');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('interact_as_actor_id')) {
|
||||
/**
|
||||
* Sets the podcast id of which the user is acting as
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
function interact_as_actor_id()
|
||||
{
|
||||
$authenticate = Services::authentication();
|
||||
$authenticate->check();
|
||||
|
||||
$session = session();
|
||||
return $session->get('interact_as_actor_id');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('interact_as_actor')) {
|
||||
/**
|
||||
* Get the actor the user is currently interacting as
|
||||
*
|
||||
* @return \ActivityPub\Entities\Actor|false
|
||||
*/
|
||||
function interact_as_actor()
|
||||
{
|
||||
$authenticate = Services::authentication();
|
||||
$authenticate->check();
|
||||
|
||||
$session = session();
|
||||
if ($session->has('interact_as_actor_id')) {
|
||||
return (new ActorModel())->getActorById(
|
||||
$session->get('interact_as_actor_id'),
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('can_user_interact')) {
|
||||
/**
|
||||
* @return bool
|
||||
* @throws DataException
|
||||
*/
|
||||
function can_user_interact()
|
||||
{
|
||||
return interact_as_actor() ? true : false;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,34 +30,34 @@ if (!function_exists('button')) {
|
|||
'size' => 'base',
|
||||
'iconLeft' => null,
|
||||
'iconRight' => null,
|
||||
'isRoundedFull' => false,
|
||||
'isSquared' => false,
|
||||
];
|
||||
$options = array_merge($defaultOptions, $customOptions);
|
||||
|
||||
$baseClass =
|
||||
'inline-flex items-center shadow-xs outline-none focus:shadow-outline';
|
||||
'inline-flex items-center font-semibold shadow-xs rounded-full focus:outline-none focus:ring';
|
||||
|
||||
$variantClass = [
|
||||
'default' => 'bg-gray-300 hover:bg-gray-400',
|
||||
'primary' => 'text-white bg-green-500 hover:bg-green-600',
|
||||
'default' => 'text-black bg-gray-300 hover:bg-gray-400',
|
||||
'primary' => 'text-white bg-pine-700 hover:bg-pine-800',
|
||||
'secondary' => 'text-white bg-gray-700 hover:bg-gray-800',
|
||||
'accent' => 'text-white bg-rose-600 hover:bg-rose-800',
|
||||
'success' => 'text-white bg-green-600 hover:bg-green-700',
|
||||
'danger' => 'text-white bg-red-600 hover:bg-red-700',
|
||||
'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600',
|
||||
'info' => 'text-white bg-teal-500 hover:bg-teal-600',
|
||||
'info' => 'text-white bg-blue-500 hover:bg-blue-600',
|
||||
];
|
||||
|
||||
$sizeClass = [
|
||||
'small' => 'text-xs md:text-sm ',
|
||||
'small' => 'text-xs md:text-sm',
|
||||
'base' => 'text-sm md:text-base',
|
||||
'large' => 'text-lg md:text-xl',
|
||||
];
|
||||
|
||||
$basePaddings = [
|
||||
'small' => 'px-1 md:px-2 md:py-1',
|
||||
'base' => 'px-2 py-1 md:px-3 md:py-2',
|
||||
'large' => 'px-3 py-2 md:px-4 md:py-2',
|
||||
'small' => 'px-2 md:px-3 md:py-1',
|
||||
'base' => 'px-3 py-1 md:px-4 md:py-2',
|
||||
'large' => 'px-3 py-2 md:px-5',
|
||||
];
|
||||
|
||||
$squaredPaddings = [
|
||||
|
|
@ -66,20 +66,9 @@ if (!function_exists('button')) {
|
|||
'large' => 'p-3',
|
||||
];
|
||||
|
||||
$roundedClass = [
|
||||
'full' => 'rounded-full',
|
||||
'small' => 'rounded-sm md:rounded',
|
||||
'base' => 'rounded md:rounded-md',
|
||||
'large' => 'rounded-md md:rounded-lg',
|
||||
];
|
||||
|
||||
$buttonClass =
|
||||
$baseClass .
|
||||
' ' .
|
||||
($options['isRoundedFull']
|
||||
? $roundedClass['full']
|
||||
: $roundedClass[$options['size']]) .
|
||||
' ' .
|
||||
($options['isSquared']
|
||||
? $squaredPaddings[$options['size']]
|
||||
: $basePaddings[$options['size']]) .
|
||||
|
|
@ -109,23 +98,23 @@ if (!function_exists('button')) {
|
|||
[
|
||||
'class' => $buttonClass,
|
||||
],
|
||||
$customAttributes
|
||||
)
|
||||
$customAttributes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$defaultButtonAttributes = [
|
||||
'type' => 'button',
|
||||
];
|
||||
$attributes = array_merge($defaultButtonAttributes, $customAttributes);
|
||||
$attributes = stringify_attributes(
|
||||
array_merge($defaultButtonAttributes, $customAttributes),
|
||||
);
|
||||
|
||||
return '<button class="' .
|
||||
$buttonClass .
|
||||
'"' .
|
||||
stringify_attributes($attributes) .
|
||||
'>' .
|
||||
$label .
|
||||
'</button>';
|
||||
return <<<HTML
|
||||
<button class="$buttonClass" $attributes>
|
||||
$label
|
||||
</button>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +141,6 @@ if (!function_exists('icon_button')) {
|
|||
$customAttributes = []
|
||||
): string {
|
||||
$defaultOptions = [
|
||||
'isRoundedFull' => true,
|
||||
'isSquared' => true,
|
||||
];
|
||||
$options = array_merge($defaultOptions, $customOptions);
|
||||
|
|
@ -185,7 +173,7 @@ if (!function_exists('hint_tooltip')) {
|
|||
$tooltip =
|
||||
'<span data-toggle="tooltip" data-placement="bottom" tabindex="0" title="' .
|
||||
$hintText .
|
||||
'" class="inline-block align-middle outline-none focus:shadow-outline';
|
||||
'" class="inline-block text-gray-500 align-middle outline-none focus:ring';
|
||||
|
||||
if ($class !== '') {
|
||||
$tooltip .= ' ' . $class;
|
||||
|
|
@ -223,8 +211,8 @@ if (!function_exists('data_table')) {
|
|||
'cell_start' => '<td class="px-4 py-2">',
|
||||
'cell_alt_start' => '<td class="px-4 py-2">',
|
||||
|
||||
'row_start' => '<tr class="bg-gray-100 hover:bg-green-100">',
|
||||
'row_alt_start' => '<tr class="hover:bg-green-100">',
|
||||
'row_start' => '<tr class="bg-gray-100 hover:bg-pine-100">',
|
||||
'row_alt_start' => '<tr class="hover:bg-pine-100">',
|
||||
];
|
||||
|
||||
$table->setTemplate($template);
|
||||
|
|
@ -276,8 +264,8 @@ if (!function_exists('publication_pill')) {
|
|||
): string {
|
||||
$class =
|
||||
$publicationStatus === 'published'
|
||||
? 'text-green-500 border-green-500'
|
||||
: 'text-orange-600 border-orange-600';
|
||||
? 'text-pine-500 border-pine-500'
|
||||
: 'text-red-600 border-red-600';
|
||||
|
||||
$transParam = [];
|
||||
if ($publicationDate) {
|
||||
|
|
@ -294,10 +282,10 @@ if (!function_exists('publication_pill')) {
|
|||
|
||||
$label = lang(
|
||||
'Episode.publication_status.' . $publicationStatus,
|
||||
$transParam
|
||||
$transParam,
|
||||
);
|
||||
|
||||
return '<span class="px-1 border ' .
|
||||
return '<span class="px-1 font-semibold border ' .
|
||||
$class .
|
||||
' ' .
|
||||
$customClass .
|
||||
|
|
@ -309,6 +297,56 @@ if (!function_exists('publication_pill')) {
|
|||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('publication_button')) {
|
||||
/**
|
||||
* Publication button component
|
||||
*
|
||||
* Displays the appropriate publication button depending on the publication status.
|
||||
*
|
||||
* @param integer $podcastId
|
||||
* @param integer $episodeId
|
||||
* @param boolean $publicationStatus the episode's publication status *
|
||||
* @return string
|
||||
*/
|
||||
function publication_button(
|
||||
$podcastId,
|
||||
$episodeId,
|
||||
$publicationStatus
|
||||
): string {
|
||||
switch ($publicationStatus) {
|
||||
case 'not_published':
|
||||
$label = lang('Episode.publish');
|
||||
$route = route_to('episode-publish', $podcastId, $episodeId);
|
||||
$variant = 'primary';
|
||||
$iconLeft = 'upload-cloud';
|
||||
break;
|
||||
case 'scheduled':
|
||||
$label = lang('Episode.publish_edit');
|
||||
$route = route_to(
|
||||
'episode-publish_edit',
|
||||
$podcastId,
|
||||
$episodeId,
|
||||
);
|
||||
$variant = 'accent';
|
||||
$iconLeft = 'upload-cloud';
|
||||
break;
|
||||
case 'published':
|
||||
$label = lang('Episode.unpublish');
|
||||
$route = route_to('episode-unpublish', $podcastId, $episodeId);
|
||||
$variant = 'danger';
|
||||
$iconLeft = 'cloud-off';
|
||||
break;
|
||||
}
|
||||
|
||||
return button($label, $route, [
|
||||
'variant' => $variant,
|
||||
'iconLeft' => $iconLeft,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('episode_numbering')) {
|
||||
/**
|
||||
* Returns relevant translated episode numbering.
|
||||
|
|
@ -387,21 +425,16 @@ if (!function_exists('location_link')) {
|
|||
$link = '';
|
||||
|
||||
if (!empty($locationName)) {
|
||||
$link = button(
|
||||
$locationName,
|
||||
$link = anchor(
|
||||
location_url($locationName, $locationGeo, $locationOsmid),
|
||||
[
|
||||
'variant' => 'default',
|
||||
'size' => 'small',
|
||||
'isRoundedFull' => true,
|
||||
'iconLeft' => 'map-pin',
|
||||
],
|
||||
icon('map-pin', 'mr-2') . $locationName,
|
||||
[
|
||||
'class' =>
|
||||
'text-gray-800' . (empty($class) ? '' : " $class"),
|
||||
'inline-flex items-baseline hover:underline' .
|
||||
(empty($class) ? '' : " $class"),
|
||||
'target' => '_blank',
|
||||
'rel' => 'noreferrer noopener',
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -17,25 +19,31 @@ use CodeIgniter\HTTP\ResponseInterface;
|
|||
*
|
||||
* @return string The episode's file path in media root
|
||||
*/
|
||||
function save_podcast_media($file, $podcast_name, $media_name)
|
||||
function save_media($file, $folder, $mediaName)
|
||||
{
|
||||
$file_name = $media_name . '.' . $file->getExtension();
|
||||
$file_name = $mediaName . '.' . $file->getExtension();
|
||||
|
||||
$mediaRoot = config('App')->mediaRoot;
|
||||
$mediaRoot = config('App')->mediaRoot . '/' . $folder;
|
||||
|
||||
if (!file_exists($mediaRoot . '/' . $podcast_name)) {
|
||||
mkdir($mediaRoot . '/' . $podcast_name, 0777, true);
|
||||
touch($mediaRoot . '/' . $podcast_name . '/index.html');
|
||||
if (!file_exists($mediaRoot)) {
|
||||
mkdir($mediaRoot, 0777, true);
|
||||
touch($mediaRoot . '/index.html');
|
||||
}
|
||||
|
||||
// move to media folder and overwrite file if already existing
|
||||
$file->move($mediaRoot . '/' . $podcast_name . '/', $file_name, true);
|
||||
$file->move($mediaRoot . '/', $file_name, true);
|
||||
|
||||
return $podcast_name . '/' . $file_name;
|
||||
return $folder . '/' . $file_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileUrl
|
||||
* @return File
|
||||
*/
|
||||
function download_file($fileUrl)
|
||||
{
|
||||
var_dump($fileUrl);
|
||||
|
||||
$client = \Config\Services::curlrequest();
|
||||
$uri = new \CodeIgniter\HTTP\URI($fileUrl);
|
||||
|
||||
|
|
@ -58,11 +66,11 @@ function download_file($fileUrl)
|
|||
ResponseInterface::HTTP_TEMPORARY_REDIRECT,
|
||||
ResponseInterface::HTTP_PERMANENT_REDIRECT,
|
||||
],
|
||||
true
|
||||
true,
|
||||
)
|
||||
) {
|
||||
$newFileUrl = (string) trim(
|
||||
$response->getHeader('location')->getValue()
|
||||
$response->getHeader('location')->getValue(),
|
||||
);
|
||||
$newLocation = new \CodeIgniter\HTTP\URI($newFileUrl);
|
||||
$response = $client->get($newLocation, [
|
||||
|
|
|
|||
|
|
@ -167,3 +167,5 @@ if (!function_exists('format_duration')) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -9,87 +9,40 @@
|
|||
/**
|
||||
* Fetches persons from an episode
|
||||
*
|
||||
* @param array $podcast_persons
|
||||
* @param array &$persons
|
||||
* @param array $persons
|
||||
* @param array &$personsArray
|
||||
*/
|
||||
function constructs_podcast_person_array($podcast_persons, &$persons)
|
||||
function construct_person_array($persons, &$personsArray)
|
||||
{
|
||||
foreach ($podcast_persons as $podcastPerson) {
|
||||
if (array_key_exists($podcastPerson->person->id, $persons)) {
|
||||
$persons[$podcastPerson->person->id]['roles'] .=
|
||||
empty($podcastPerson->person_group) ||
|
||||
empty($podcastPerson->person_role)
|
||||
foreach ($persons as $person) {
|
||||
if (array_key_exists($person->person->id, $personsArray)) {
|
||||
$personsArray[$person->person->id]['roles'] .=
|
||||
empty($person->person_group) || empty($person->person_role)
|
||||
? ''
|
||||
: (empty($persons[$podcastPerson->person->id]['roles'])
|
||||
: (empty($personsArray[$person->person->id]['roles'])
|
||||
? ''
|
||||
: ', ') .
|
||||
lang(
|
||||
'PersonsTaxonomy.persons.' .
|
||||
$podcastPerson->person_group .
|
||||
$person->person_group .
|
||||
'.roles.' .
|
||||
$podcastPerson->person_role .
|
||||
'.label'
|
||||
$person->person_role .
|
||||
'.label',
|
||||
);
|
||||
} else {
|
||||
$persons[$podcastPerson->person->id] = [
|
||||
'full_name' => $podcastPerson->person->full_name,
|
||||
'information_url' => $podcastPerson->person->information_url,
|
||||
'thumbnail_url' => $podcastPerson->person->image->thumbnail_url,
|
||||
$personsArray[$person->person->id] = [
|
||||
'full_name' => $person->person->full_name,
|
||||
'information_url' => $person->person->information_url,
|
||||
'thumbnail_url' => $person->person->image->thumbnail_url,
|
||||
'roles' =>
|
||||
empty($podcastPerson->person_group) ||
|
||||
empty($podcastPerson->person_role)
|
||||
empty($person->person_group) || empty($person->person_role)
|
||||
? ''
|
||||
: lang(
|
||||
'PersonsTaxonomy.persons.' .
|
||||
$podcastPerson->person_group .
|
||||
$person->person_group .
|
||||
'.roles.' .
|
||||
$podcastPerson->person_role .
|
||||
'.label'
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches persons from an episode
|
||||
*
|
||||
* @param array $episode_persons
|
||||
* @param array &$persons
|
||||
*/
|
||||
function construct_episode_person_array($episode_persons, &$persons)
|
||||
{
|
||||
foreach ($episode_persons as $episodePerson) {
|
||||
if (array_key_exists($episodePerson->person->id, $persons)) {
|
||||
$persons[$episodePerson->person->id]['roles'] .=
|
||||
empty($episodePerson->person_group) ||
|
||||
empty($episodePerson->person_role)
|
||||
? ''
|
||||
: (empty($persons[$episodePerson->person->id]['roles'])
|
||||
? ''
|
||||
: ', ') .
|
||||
lang(
|
||||
'PersonsTaxonomy.persons.' .
|
||||
$episodePerson->person_group .
|
||||
'.roles.' .
|
||||
$episodePerson->person_role .
|
||||
'.label'
|
||||
);
|
||||
} else {
|
||||
$persons[$episodePerson->person->id] = [
|
||||
'full_name' => $episodePerson->person->full_name,
|
||||
'information_url' => $episodePerson->person->information_url,
|
||||
'thumbnail_url' => $episodePerson->person->image->thumbnail_url,
|
||||
'roles' =>
|
||||
empty($episodePerson->person_group) ||
|
||||
empty($episodePerson->person_role)
|
||||
? ''
|
||||
: lang(
|
||||
'PersonsTaxonomy.persons.' .
|
||||
$episodePerson->person_group .
|
||||
'.roles.' .
|
||||
$episodePerson->person_role .
|
||||
'.label'
|
||||
$person->person_role .
|
||||
'.label',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ function get_rss_feed($podcast, $serviceSlug = '')
|
|||
}
|
||||
}
|
||||
|
||||
foreach ($podcast->podcast_persons as $podcastPerson) {
|
||||
foreach ($podcast->persons as $podcastPerson) {
|
||||
$podcastPersonElement = $channel->addChild(
|
||||
'person',
|
||||
htmlspecialchars($podcastPerson->person->full_name),
|
||||
|
|
@ -358,7 +358,7 @@ function get_rss_feed($podcast, $serviceSlug = '')
|
|||
$soundbiteElement->addAttribute('duration', $soundbite->duration);
|
||||
}
|
||||
|
||||
foreach ($episode->episode_persons as $episodePerson) {
|
||||
foreach ($episode->persons as $episodePerson) {
|
||||
$episodePersonElement = $item->addChild(
|
||||
'person',
|
||||
htmlspecialchars($episodePerson->person->full_name),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function icon(string $name, string $class = '')
|
|||
$svg_contents = str_replace(
|
||||
'<svg',
|
||||
'<svg class="' . $class . '"',
|
||||
$svg_contents
|
||||
$svg_contents,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -41,36 +41,7 @@ function svg($name, $class = null)
|
|||
$svg_contents = str_replace(
|
||||
'<svg',
|
||||
'<svg class="' . $class . '"',
|
||||
$svg_contents
|
||||
);
|
||||
}
|
||||
return $svg_contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inline svg platform icon. Returns the default icon if not found.
|
||||
*
|
||||
* @param string $name name of the image file without the .svg extension
|
||||
* @param string $class to be added to the svg string
|
||||
* @return string svg contents
|
||||
*/
|
||||
function platform_icon($type, $name, $class = null)
|
||||
{
|
||||
try {
|
||||
$svg_contents = file_get_contents(
|
||||
'assets/images/platforms/' . $type . '/' . $name . '.svg'
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
$svg_contents = file_get_contents(
|
||||
'assets/images/platforms/_default.svg'
|
||||
);
|
||||
}
|
||||
|
||||
if ($class) {
|
||||
$svg_contents = str_replace(
|
||||
'<svg',
|
||||
'<svg class="' . $class . '"',
|
||||
$svg_contents
|
||||
$svg_contents,
|
||||
);
|
||||
}
|
||||
return $svg_contents;
|
||||
|
|
|
|||
|
|
@ -74,3 +74,34 @@ if (!function_exists('location_url')) {
|
|||
return $uri;
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('extract_params_from_episode_uri')) {
|
||||
/**
|
||||
* Returns podcast name and episode slug from episode string uri
|
||||
*
|
||||
* @param URI $episodeUri
|
||||
* @return string|null
|
||||
*/
|
||||
function extract_params_from_episode_uri($episodeUri)
|
||||
{
|
||||
preg_match(
|
||||
'/@(?P<podcastName>[a-zA-Z0-9\_]{1,32})\/episodes\/(?P<episodeSlug>[a-zA-Z0-9\-]{1,191})/',
|
||||
$episodeUri->getPath(),
|
||||
$matches
|
||||
);
|
||||
|
||||
if (
|
||||
$matches &&
|
||||
array_key_exists('podcastName', $matches) &&
|
||||
array_key_exists('episodeSlug', $matches)
|
||||
) {
|
||||
return [
|
||||
'podcastName' => $matches['podcastName'],
|
||||
'episodeSlug' => $matches['episodeSlug'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
app/Language/en/ActivityPub.php
Normal file
34
app/Language/en/ActivityPub.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'your_handle' => 'Your handle',
|
||||
'your_handle_hint' => 'Enter the @username@domain you want to act from.',
|
||||
'follow' => [
|
||||
'label' => 'Follow',
|
||||
'title' => 'Follow {actorDisplayName}',
|
||||
'subtitle' => 'You are going to follow:',
|
||||
'accountNotFound' => 'The account could not be found.',
|
||||
'submit' => 'Proceed to follow',
|
||||
],
|
||||
'favourite' => [
|
||||
'title' => 'Favourite {actorDisplayName}\'s note',
|
||||
'subtitle' => 'You are going to favourite:',
|
||||
'submit' => 'Proceed to favourite',
|
||||
],
|
||||
'reblog' => [
|
||||
'title' => 'Share {actorDisplayName}\'s note',
|
||||
'subtitle' => 'You are going to share:',
|
||||
'submit' => 'Proceed to share',
|
||||
],
|
||||
'reply' => [
|
||||
'title' => 'Reply to {actorDisplayName}\'s note',
|
||||
'subtitle' => 'You are going to reply to:',
|
||||
'submit' => 'Proceed to reply',
|
||||
],
|
||||
];
|
||||
|
|
@ -9,4 +9,5 @@
|
|||
return [
|
||||
'dashboard' => 'Admin dashboard',
|
||||
'welcome_message' => 'Welcome to the admin area!',
|
||||
'choose_interact' => 'Choose how to interact',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ return [
|
|||
'persons' => 'Persons',
|
||||
'person-list' => 'All persons',
|
||||
'person-create' => 'New person',
|
||||
'fediverse' => 'Fediverse',
|
||||
'fediverse-blocked-actors' => 'Blocked accounts',
|
||||
'fediverse-blocked-domains' => 'Blocked domains',
|
||||
'users' => 'Users',
|
||||
'user-list' => 'All users',
|
||||
'user-create' => 'New user',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ return [
|
|||
'new' => 'new',
|
||||
'edit' => 'edit',
|
||||
'persons' => 'persons',
|
||||
'publish' => 'publish',
|
||||
'publish-edit' => 'edit publication',
|
||||
'unpublish' => 'unpublish',
|
||||
'fediverse' => 'fediverse',
|
||||
'block-lists' => 'block lists',
|
||||
'users' => 'users',
|
||||
'my-account' => 'my account',
|
||||
'change-password' => 'change password',
|
||||
|
|
|
|||
|
|
@ -9,14 +9,18 @@
|
|||
return [
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
'cancel' => 'Cancel',
|
||||
'optional' => 'Optional',
|
||||
'more' => 'More',
|
||||
'no_data' => 'No data found!',
|
||||
'close' => 'Close',
|
||||
'home' => 'Home',
|
||||
'explicit' => 'Explicit',
|
||||
'mediumDate' => '{0,date,medium}',
|
||||
'powered_by' => 'Powered by {castopod}.',
|
||||
'actions' => 'Actions',
|
||||
'pageInfo' => 'Page {currentPage} out of {pageCount}',
|
||||
'go_back' => 'Go back',
|
||||
'forms' => [
|
||||
'multiSelect' => [
|
||||
'selectText' => 'Press to select',
|
||||
|
|
|
|||
|
|
@ -7,19 +7,33 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'previous_episode' => 'Previous episode',
|
||||
'previous_season' => 'Previous season',
|
||||
'next_episode' => 'Next episode',
|
||||
'next_season' => 'Next season',
|
||||
'season' => 'Season {seasonNumber}',
|
||||
'season_abbr' => 'S{seasonNumber}',
|
||||
'number' => 'Episode {episodeNumber}',
|
||||
'number_abbr' => 'Ep. {episodeNumber}',
|
||||
'season_episode' => 'Season {seasonNumber} episode {episodeNumber}',
|
||||
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
|
||||
'back_to_episodes' => 'Back to episodes of {podcast}',
|
||||
'activity' => 'Activity',
|
||||
'description' => 'Description',
|
||||
'total_favourites' => '{numberOfTotalFavourites, plural,
|
||||
one {# total favourite}
|
||||
other {# total favourites}
|
||||
}',
|
||||
'total_reblogs' => '{numberOfTotalReblogs, plural,
|
||||
one {# total share}
|
||||
other {# total shares}
|
||||
}',
|
||||
'total_notes' => '{numberOfTotalNotes, plural,
|
||||
one {# note}
|
||||
other {# total notes}
|
||||
}',
|
||||
'all_podcast_episodes' => 'All podcast episodes',
|
||||
'back_to_podcast' => 'Go back to podcast',
|
||||
'edit' => 'Edit',
|
||||
'publish' => 'Publish',
|
||||
'publish_edit' => 'Edit publication',
|
||||
'unpublish' => 'Unpublish',
|
||||
'delete' => 'Delete',
|
||||
'go_to_page' => 'Go to page',
|
||||
'create' => 'Add an episode',
|
||||
|
|
@ -51,19 +65,6 @@ return [
|
|||
'trailer' => 'Trailer',
|
||||
'bonus' => 'Bonus',
|
||||
],
|
||||
'show_notes_section_title' => 'Show notes',
|
||||
'show_notes_section_subtitle' =>
|
||||
'Up to 4000 characters, be clear and concise. Show notes help potential listeners in finding the episode.',
|
||||
'description' => 'Description',
|
||||
'description_footer' => 'Description footer',
|
||||
'description_footer_hint' =>
|
||||
'This text is added at the end of each episode description, it is a good place to input your social links for example.',
|
||||
'publication_section_title' => 'Publication info',
|
||||
'publication_section_subtitle' => '',
|
||||
'publication_date' => 'Publication date',
|
||||
'publication_date_clear' => 'Clear publication date',
|
||||
'publication_date_hint' =>
|
||||
'You can schedule the episode release by setting a future publication date. This field must be formatted as YYYY-MM-DD HH:mm',
|
||||
'parental_advisory' => [
|
||||
'label' => 'Parental advisory',
|
||||
'hint' => 'Does the episode contain explicit content?',
|
||||
|
|
@ -71,30 +72,59 @@ return [
|
|||
'clean' => 'Clean',
|
||||
'explicit' => 'Explicit',
|
||||
],
|
||||
'block' => 'Episode should be hidden from all platforms',
|
||||
'block_hint' =>
|
||||
'The episode show or hide status. If you want this episode removed from the Apple directory, toggle this on.',
|
||||
'show_notes_section_title' => 'Show notes',
|
||||
'show_notes_section_subtitle' =>
|
||||
'Up to 4000 characters, be clear and concise. Show notes help potential listeners in finding the episode.',
|
||||
'description' => 'Description',
|
||||
'description_footer' => 'Description footer',
|
||||
'description_footer_hint' =>
|
||||
'This text is added at the end of each episode description, it is a good place to input your social links for example.',
|
||||
'additional_files_section_title' => 'Additional files',
|
||||
'additional_files_section_subtitle' =>
|
||||
'These files may be used by other platforms to provide better experience to your audience.<br />See the {podcastNamespaceLink} for more information.',
|
||||
'location_section_title' => 'Location',
|
||||
'location_section_subtitle' => 'What place is this episode about?',
|
||||
'location_name' => 'Location name or address',
|
||||
'location_name_hint' => 'This can be a real or fictional location',
|
||||
'transcript' => 'Transcript or closed captions',
|
||||
'transcript_hint' => 'Allowed formats are txt, html, srt or json.',
|
||||
'transcript_delete' => 'Delete transcript',
|
||||
'chapters' => 'Chapters',
|
||||
'chapters_hint' => 'File should be in JSON Chapters Format.',
|
||||
'chapters_delete' => 'Delete chapters',
|
||||
'location_section_title' => 'Location',
|
||||
'location_section_subtitle' => 'What place is this episode about?',
|
||||
'location_name' => 'Location name or address',
|
||||
'location_name_hint' => 'This can be a real place or fictional',
|
||||
'advanced_section_title' => 'Advanced Parameters',
|
||||
'advanced_section_subtitle' =>
|
||||
'If you need RSS tags that Castopod does not handle, set them here.',
|
||||
'custom_rss' => 'Custom RSS tags for the episode',
|
||||
'custom_rss_hint' => 'This will be injected within the ❬item❭ tag.',
|
||||
'block' => 'Episode should be hidden from all platforms',
|
||||
'block_hint' =>
|
||||
'The episode show or hide status. If you want this episode removed from the Apple directory, toggle this on.',
|
||||
'submit_create' => 'Create episode',
|
||||
'submit_edit' => 'Save episode',
|
||||
],
|
||||
'publish_form' => [
|
||||
'note' => 'Your note',
|
||||
'note_hint' =>
|
||||
'The message you write will be broadcasted to all your followers in the fediverse.',
|
||||
'publication_date' => 'Publication date',
|
||||
'publication_method' => [
|
||||
'now' => 'Now',
|
||||
'schedule' => 'Schedule',
|
||||
],
|
||||
'scheduled_publication_date' => 'Scheduled publication date',
|
||||
'scheduled_publication_date_clear' => 'Clear publication date',
|
||||
'scheduled_publication_date_hint' =>
|
||||
'You can schedule the episode release by setting a future publication date. This field must be formatted as YYYY-MM-DD HH:mm',
|
||||
'submit' => 'Publish',
|
||||
'submit_edit' => 'Edit publication',
|
||||
],
|
||||
'unpublish_form' => [
|
||||
'disclaimer' =>
|
||||
'Unpublishing the episode will delete all the notes associated with the episode and remove it from the podcast\'s RSS feed.',
|
||||
'understand' => 'I understand, I want to unpublish the episode',
|
||||
'submit' => 'Unpublish',
|
||||
],
|
||||
'soundbites' => 'Soundbites',
|
||||
'soundbites_form' => [
|
||||
'title' => 'Edit soundbites',
|
||||
|
|
|
|||
23
app/Language/en/Fediverse.php
Normal file
23
app/Language/en/Fediverse.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'blocked_actors' => 'Blocked accounts',
|
||||
'blocked_domains' => 'Blocked domains',
|
||||
'block_lists_form' => [
|
||||
'handle' => 'Account handle',
|
||||
'handle_hint' => 'Input @username@domain account.',
|
||||
'domain' => 'Domain name',
|
||||
'submit' => 'Block!',
|
||||
],
|
||||
'list' => [
|
||||
'actor' => 'Account',
|
||||
'domain' => 'Domain name',
|
||||
'unblock' => 'Unblock',
|
||||
],
|
||||
];
|
||||
38
app/Language/en/Note.php
Normal file
38
app/Language/en/Note.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'title' => '{actorDisplayName}\'s Note',
|
||||
'back_to_actor_notes' => 'Back to {actor} notes',
|
||||
'actor_shared' => '{actor} shared',
|
||||
'reply_to' => 'Reply to @{actorUsername}',
|
||||
'form' => [
|
||||
'message_placeholder' => 'Write a message...',
|
||||
'episode_message_placeholder' => 'Write a message for the episode...',
|
||||
'episode_url_placeholder' => 'Episode URL',
|
||||
'reply_to_placeholder' => 'Reply to @{actorUsername}',
|
||||
'submit' => 'Send!',
|
||||
'submit_reply' => 'Reply',
|
||||
],
|
||||
'favourites' => '{numberOfFavourites, plural,
|
||||
one {# favourite}
|
||||
other {# favourites}
|
||||
}',
|
||||
'reblogs' => '{numberOfReblogs, plural,
|
||||
one {# share}
|
||||
other {# shares}
|
||||
}',
|
||||
'replies' => '{numberOfReplies, plural,
|
||||
one {# reply}
|
||||
other {# replies}
|
||||
}',
|
||||
'expand' => 'Expand note',
|
||||
'block_actor' => 'Block user @{actorUsername}',
|
||||
'block_domain' => 'Block domain @{actorDomain}',
|
||||
'delete' => 'Delete note',
|
||||
];
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'back_to_home' => 'Back to home',
|
||||
'page' => 'Page',
|
||||
'all_pages' => 'All pages',
|
||||
'create' => 'New page',
|
||||
|
|
@ -21,6 +22,6 @@ return [
|
|||
'submit_edit' => 'Save',
|
||||
],
|
||||
'messages' => [
|
||||
'createSuccess' => 'The {pageTitle} page was created successfully!',
|
||||
'createSuccess' => 'The page "{pageTitle}" was created successfully!',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -209,9 +209,26 @@ return [
|
|||
],
|
||||
'by' => 'By {publisher}',
|
||||
'season' => 'Season {seasonNumber}',
|
||||
'list_of_episodes_year' => '{year} episodes',
|
||||
'list_of_episodes_season' => 'Season {seasonNumber} episodes',
|
||||
'list_of_episodes_year' => '{year} episodes ({episodeCount})',
|
||||
'list_of_episodes_season' =>
|
||||
'Season {seasonNumber} episodes ({episodeCount})',
|
||||
'no_episode' => 'No episode found!',
|
||||
'no_episode_hint' =>
|
||||
'Navigate the podcast episodes with the navigation bar above.',
|
||||
'follow' => 'Follow',
|
||||
'followers' => '{numberOfFollowers, plural,
|
||||
one {<span class="font-semibold">#</span> follower}
|
||||
other {<span class="font-semibold">#</span> followers}
|
||||
}',
|
||||
'notes' => '{numberOfNotes, plural,
|
||||
one {<span class="font-semibold">#</span> note}
|
||||
other {<span class="font-semibold">#</span> notes}
|
||||
}',
|
||||
'activity' => 'Activity',
|
||||
'episodes' => 'Episodes',
|
||||
'sponsor_title' => 'Enjoying the show?',
|
||||
'sponsor' => 'Sponsor',
|
||||
'funding_links' => 'Funding links for {podcastTitle}',
|
||||
'find_on' => 'Find {podcastTitle} on',
|
||||
'listen_on' => 'Listen on',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ return [
|
|||
'episodes' => 'Episodes',
|
||||
'episode-list' => 'All episodes',
|
||||
'episode-create' => 'New episode',
|
||||
'fediverse' => 'Fediverse',
|
||||
'fediverse-block_lists' => 'Block lists',
|
||||
'analytics' => 'Analytics',
|
||||
'persons' => 'Persons',
|
||||
'podcast-person-manage' => 'Manage persons',
|
||||
|
|
|
|||
35
app/Language/fr/ActivityPub.php
Normal file
35
app/Language/fr/ActivityPub.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'your_handle' => 'Votre pseudonyme',
|
||||
'your_handle_hint' =>
|
||||
'Entrez le @utilisateur@domaine avec lequel vous voulez interagir.',
|
||||
'follow' => [
|
||||
'label' => 'Suivre',
|
||||
'title' => 'Suivre {actorDisplayName}',
|
||||
'subtitle' => 'Vous allez suivre :',
|
||||
'accountNotFound' => 'Le compte n’a pas pu être trouvé.',
|
||||
'submit' => 'Poursuivre',
|
||||
],
|
||||
'favourite' => [
|
||||
'title' => 'Mettez la note de {actorDisplayName} en favori',
|
||||
'subtitle' => 'Vous allez mettre en favori :',
|
||||
'submit' => 'Poursuivre',
|
||||
],
|
||||
'reblog' => [
|
||||
'title' => 'Partagez la note de {actorDisplayName}',
|
||||
'subtitle' => 'Vous allez partager :',
|
||||
'submit' => 'Poursuivre',
|
||||
],
|
||||
'reply' => [
|
||||
'title' => 'Répondre à la note de {actorDisplayName}',
|
||||
'subtitle' => 'Vous allez répondre à :',
|
||||
'submit' => 'Poursuivre',
|
||||
],
|
||||
];
|
||||
|
|
@ -17,6 +17,9 @@ return [
|
|||
'persons' => 'Intervenants',
|
||||
'person-list' => 'Tous les intervenants',
|
||||
'person-create' => 'Nouvel intervenant',
|
||||
'fediverse' => 'Fédiverse',
|
||||
'fediverse-blocked_actors' => 'Utilisateurs blockés',
|
||||
'fediverse-blocked_domains' => 'Domaines blockés',
|
||||
'users' => 'Utilisateurs',
|
||||
'user-list' => 'Tous les utilisateurs',
|
||||
'user-create' => 'Créer un utilisateur',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ return [
|
|||
'new' => 'créer',
|
||||
'edit' => 'modifier',
|
||||
'persons' => 'intervenants',
|
||||
'publish' => 'publier',
|
||||
'publish-edit' => 'modifier la publication',
|
||||
'unpublish' => 'dépublier',
|
||||
'fediverse' => 'fédiverse',
|
||||
'block-lists' => 'listes de blocage',
|
||||
'users' => 'utilisateurs',
|
||||
'my-account' => 'mon compte',
|
||||
'change-password' => 'changer le mot de passe',
|
||||
|
|
|
|||
|
|
@ -9,14 +9,18 @@
|
|||
return [
|
||||
'yes' => 'Oui',
|
||||
'no' => 'Non',
|
||||
'cancel' => 'Annuler',
|
||||
'optional' => 'Optionnel',
|
||||
'more' => 'Plus',
|
||||
'no_data' => 'Aucune donnée trouvée !',
|
||||
'close' => 'Fermer',
|
||||
'home' => 'Accueil',
|
||||
'explicit' => 'Explicite',
|
||||
'mediumDate' => '{0,date,medium}',
|
||||
'powered_by' => 'Propulsé par {castopod}.',
|
||||
'actions' => 'Actions',
|
||||
'pageInfo' => 'Page {currentPage} sur {pageCount}',
|
||||
'go_back' => 'Retour en arrière',
|
||||
'forms' => [
|
||||
'multiSelect' => [
|
||||
'selectText' => 'Cliquez pour selectionner',
|
||||
|
|
|
|||
|
|
@ -7,19 +7,33 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'previous_episode' => 'Épisode précédent',
|
||||
'previous_season' => 'Saison précédente',
|
||||
'next_episode' => 'Épisode suivant',
|
||||
'next_season' => 'Saison suivante',
|
||||
'season' => 'Saison {seasonNumber}',
|
||||
'season_abbr' => 'S{seasonNumber}',
|
||||
'number' => 'Épisode {episodeNumber}',
|
||||
'number_abbr' => 'Ep. {episodeNumber}',
|
||||
'season_episode' => 'Saison {seasonNumber} épisode {episodeNumber}',
|
||||
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
|
||||
'back_to_episodes' => 'Retour aux épisodes de {podcast}',
|
||||
'activity' => 'Activité',
|
||||
'description' => 'Description',
|
||||
'total_favourites' => '{numberOfTotalFavourites, plural,
|
||||
one {# favori en tout}
|
||||
other {# favoris en tout}
|
||||
}',
|
||||
'total_reblogs' => '{numberOfTotalReblogs, plural,
|
||||
one {# partage en tout}
|
||||
other {# partages en tout}
|
||||
}',
|
||||
'total_notes' => '{numberOfTotalNotes, plural,
|
||||
one {# note}
|
||||
other {# notes}
|
||||
}',
|
||||
'all_podcast_episodes' => 'Tous les épisodes du podcast',
|
||||
'back_to_podcast' => 'Revenir au podcast',
|
||||
'edit' => 'Modifier',
|
||||
'publish' => 'Publier',
|
||||
'publish_edit' => 'Modifier la publication',
|
||||
'unpublish' => 'Dépublier',
|
||||
'delete' => 'Supprimer',
|
||||
'go_to_page' => 'Voir',
|
||||
'create' => 'Ajouter un épisode',
|
||||
|
|
@ -51,19 +65,6 @@ return [
|
|||
'trailer' => 'Bande-annonce',
|
||||
'bonus' => 'Bonus',
|
||||
],
|
||||
'show_notes_section_title' => 'Notes d’épisode (Show Notes)',
|
||||
'show_notes_section_subtitle' =>
|
||||
'Jusque 4000 caractères, soyez clairs et concis. Les notes d’épisode aident les auditeurs potentiels à le trouver.',
|
||||
'description' => 'Description',
|
||||
'description_footer' => 'Pied de description',
|
||||
'description_footer_hint' =>
|
||||
'Ce texte est ajouté à la fin de chaque description d’épisode, c’est un bon endroit pour placer vos liens sociaux par exemple.',
|
||||
'publication_section_title' => 'Information de publication',
|
||||
'publication_section_subtitle' => '',
|
||||
'publication_date' => 'Date de publication',
|
||||
'publication_date_clear' => 'Effacer la date de publication',
|
||||
'publication_date_hint' =>
|
||||
'Vous pouvez planifier la sortie de l’épisode en saisissant une date de publication future. Ce champ doit être au format YYYY-MM-DD HH:mm',
|
||||
'parental_advisory' => [
|
||||
'label' => 'Avertissement parental',
|
||||
'hint' => 'L’épisode contient-il un contenu explicite ?',
|
||||
|
|
@ -71,12 +72,20 @@ return [
|
|||
'clean' => 'Convenable',
|
||||
'explicit' => 'Explicite',
|
||||
],
|
||||
'block' => 'L’épisode doit être masqué de toutes les plateformes',
|
||||
'block_hint' =>
|
||||
'La visibilité de l’épisode. Si vous souhaitez retirer cet épisode de l’index Apple, activez ce champ.',
|
||||
'show_notes_section_title' => 'Notes d’épisode (Show Notes)',
|
||||
'show_notes_section_subtitle' =>
|
||||
'Jusque 4000 caractères, soyez clairs et concis. Les notes d’épisode aident les auditeurs potentiels à le trouver.',
|
||||
'description' => 'Description',
|
||||
'description_footer' => 'Pied de description',
|
||||
'description_footer_hint' =>
|
||||
'Ce texte est ajouté à la fin de chaque description d’épisode, c’est un bon endroit pour placer vos liens sociaux par exemple.',
|
||||
'additional_files_section_title' => 'Fichiers additionels',
|
||||
'additional_files_section_subtitle' =>
|
||||
'Ces fichiers pourront être utilisées par d’autres plate-formes pour procurer une meilleure expérience à vos auditeurs.<br />Consulter le {podcastNamespaceLink} pour plus d’informations.',
|
||||
'location_section_title' => 'Localisation',
|
||||
'location_section_subtitle' => 'De quel lieu cet épisode parle-t-il ?',
|
||||
'location_name' => 'Nom ou adresse du lieu',
|
||||
'location_name_hint' => 'Ce lieu peut être réel ou fictif',
|
||||
'transcript' => 'Transcription ou sous-titrage',
|
||||
'transcript_hint' =>
|
||||
'Les formats autorisés sont txt, html, srt ou json.',
|
||||
|
|
@ -84,18 +93,39 @@ return [
|
|||
'chapters' => 'Chapitrage',
|
||||
'chapters_hint' => 'Le fichier doit être en "JSON Chapters Format".',
|
||||
'chapters_delete' => 'Supprimer le chapitrage',
|
||||
'location_section_title' => 'Localisation',
|
||||
'location_section_subtitle' => 'De quel lieu cet épisode parle-t-il ?',
|
||||
'location_name' => 'Nom ou adresse du lieu',
|
||||
'location_name_hint' => 'Ce lieu peut être réel ou fictif',
|
||||
'advanced_section_title' => 'Paramètres avancés',
|
||||
'advanced_section_subtitle' =>
|
||||
'Si vous avez besoin d’une balise que nous n’avons pas couverte, définissez-la ici.',
|
||||
'Si vous avez besoin d’une balise que Castopod ne couvre pas, définissez-la ici.',
|
||||
'custom_rss' => 'Balises RSS personnalisées pour l’épisode',
|
||||
'custom_rss_hint' => 'Ceci sera injecté dans la balise ❬item❭.',
|
||||
'block' => 'L’épisode doit être masqué de toutes les plateformes',
|
||||
'block_hint' =>
|
||||
'La visibilité de l’épisode. Si vous souhaitez retirer cet épisode de l’index Apple, activez ce champ.',
|
||||
'submit_create' => 'Créer l’épisode',
|
||||
'submit_edit' => 'Enregistrer l’épisode',
|
||||
],
|
||||
'publish_form' => [
|
||||
'publication_date' => 'Date de publication',
|
||||
'publication_date_clear' => 'Effacer la date de publication',
|
||||
'publication_date_hint' =>
|
||||
'Vous pouvez planifier la sortie de l’épisode en saisissant une date de publication future. Ce champ doit être au format YYYY-MM-DD HH:mm',
|
||||
],
|
||||
'publish_form' => [
|
||||
'note' => 'Votre note',
|
||||
'note_hint' =>
|
||||
'Le message que vous écrirez sera diffusé à toutes les personnes qui vous suivent dans le fédiverse.',
|
||||
'publication_date' => 'Date de publication',
|
||||
'publication_method' => [
|
||||
'now' => 'Maintenant',
|
||||
'schedule' => 'Planifier',
|
||||
],
|
||||
'scheduled_publication_date' => 'Date de publication programmée',
|
||||
'scheduled_publication_date_clear' => 'Effacer la date de publication',
|
||||
'scheduled_publication_date_hint' =>
|
||||
'Vous pouvez planifier la sortie de l’épisode en saisissant une date de publication future. Ce champ doit être au format YYYY-MM-DD HH:mm',
|
||||
'submit' => 'Publier',
|
||||
'submit_edit' => 'Modifier la publication',
|
||||
],
|
||||
'soundbites' => 'Extraits sonores',
|
||||
'soundbites_form' => [
|
||||
'title' => 'Modifier les extraits sonores',
|
||||
|
|
|
|||
20
app/Language/fr/Fediverse.php
Normal file
20
app/Language/fr/Fediverse.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'block_lists' => 'Listes de blocage',
|
||||
'block_lists_form' => [
|
||||
'blocked_users' => 'Utilisateurs bloqués',
|
||||
'blocked_users_hint' =>
|
||||
'Entrez les pseudonymes @utilisateur@domaine séparés par une virgule.',
|
||||
'blocked_domains' => 'Domaines bloqués',
|
||||
'blocked_domains_hint' =>
|
||||
'Entrez les noms de domaine séparés par une virgule.',
|
||||
'submit' => 'Sauvegarder les listes',
|
||||
],
|
||||
];
|
||||
39
app/Language/fr/Note.php
Normal file
39
app/Language/fr/Note.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'title' => 'Note de {actorDisplayName}',
|
||||
'back_to_actor_notes' => 'Retour aux notes de {actor}',
|
||||
'actor_shared' => '{actor} a partagé',
|
||||
'reply_to' => 'Répondre à @{actorUsername}',
|
||||
'form' => [
|
||||
'message_placeholder' => 'Écrivez votre message...',
|
||||
'episode_message_placeholder' =>
|
||||
'Écrivez votre message pour l’épisode...',
|
||||
'episode_url_placeholder' => 'URL de l’épisode',
|
||||
'reply_to_placeholder' => 'Répondre à @{actorUsername}',
|
||||
'submit' => 'Envoyer!',
|
||||
'submit_reply' => 'Répondre',
|
||||
],
|
||||
'favourites' => '{numberOfFavourites, plural,
|
||||
one {# favori}
|
||||
other {# favoris}
|
||||
}',
|
||||
'reblogs' => '{numberOfReblogs, plural,
|
||||
one {# partage}
|
||||
other {# partages}
|
||||
}',
|
||||
'replies' => '{numberOfReplies, plural,
|
||||
one {# réponse}
|
||||
other {# réponses}
|
||||
}',
|
||||
'expand' => 'Ouvrir la note',
|
||||
'block_actor' => 'Bloquer l’utilisateur @{actorUsername}',
|
||||
'block_domain' => 'Bloquer le domaine @{actorDomain}',
|
||||
'delete' => 'Supprimer la note',
|
||||
];
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'back_to_home' => 'Retour à l’accueil',
|
||||
'page' => 'Page',
|
||||
'all_pages' => 'Toutes les pages',
|
||||
'create' => 'Créer une page',
|
||||
|
|
|
|||
|
|
@ -211,9 +211,26 @@ return [
|
|||
],
|
||||
'by' => 'Par {publisher}',
|
||||
'season' => 'Saison {seasonNumber}',
|
||||
'list_of_episodes_year' => 'épisodes {year}',
|
||||
'list_of_episodes_season' => 'Épisodes de la saison {seasonNumber}',
|
||||
'list_of_episodes_year' => 'Épisodes de {year} (episodeCount)',
|
||||
'list_of_episodes_season' =>
|
||||
'Épisodes de la saison {seasonNumber} (episodeCount)',
|
||||
'no_episode' => 'Aucun épisode trouvé !',
|
||||
'no_episode_hint' =>
|
||||
'Naviguez au sein des épisodes du podcast episodes grâce à la barre de navigation ci-dessus.',
|
||||
'follow' => 'Suivre',
|
||||
'followers' => '{numberOfFollowers, plural,
|
||||
one {<span class="font-semibold">#</span> abonné·e}
|
||||
other {<span class="font-semibold">#</span> abonné·e·s}
|
||||
}',
|
||||
'notes' => '{numberOfNotes, plural,
|
||||
one {<span class="font-semibold">#</span> note}
|
||||
other {<span class="font-semibold">#</span> notes}
|
||||
}',
|
||||
'activity' => 'Activité',
|
||||
'episodes' => 'Épisodes',
|
||||
'sponsor_title' => 'Vous aimez le podcast ?',
|
||||
'sponsor' => 'Soutenez-nous',
|
||||
'funding_links' => 'Liens de financement pour {podcastTitle}',
|
||||
'find_on' => 'Trouvez {podcastTitle} sur',
|
||||
'listen_on' => 'Écoutez sur',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ return [
|
|||
'episodes' => 'Épisodes',
|
||||
'episode-list' => 'Tous les épisodes',
|
||||
'episode-create' => 'Créer un épisode',
|
||||
'fediverse' => 'Fédiverse',
|
||||
'fediverse-block_lists' => 'Listes de blocage',
|
||||
'analytics' => 'Mesures d’audience',
|
||||
'persons' => 'Intervenants',
|
||||
'podcast-person-manage' => 'Gestion des intervenants',
|
||||
|
|
|
|||
24
app/Libraries/ActivityPub/Activities/AcceptActivity.php
Normal file
24
app/Libraries/ActivityPub/Activities/AcceptActivity.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Activities;
|
||||
|
||||
use ActivityPub\Core\Activity;
|
||||
|
||||
class AcceptActivity extends Activity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Accept';
|
||||
}
|
||||
37
app/Libraries/ActivityPub/Activities/AnnounceActivity.php
Normal file
37
app/Libraries/ActivityPub/Activities/AnnounceActivity.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Activities;
|
||||
|
||||
use ActivityPub\Core\Activity;
|
||||
|
||||
class AnnounceActivity extends Activity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Announce';
|
||||
|
||||
public function __construct($reblogNote)
|
||||
{
|
||||
$this->actor = $reblogNote->actor->uri;
|
||||
$this->object = $reblogNote->reblog_of_note->uri;
|
||||
|
||||
$this->published = $reblogNote->published_at->format(DATE_W3C);
|
||||
|
||||
$this->cc = [
|
||||
$reblogNote->actor->uri,
|
||||
$reblogNote->actor->followers_url,
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Libraries/ActivityPub/Activities/CreateActivity.php
Normal file
24
app/Libraries/ActivityPub/Activities/CreateActivity.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Activities;
|
||||
|
||||
use ActivityPub\Core\Activity;
|
||||
|
||||
class CreateActivity extends Activity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Create';
|
||||
}
|
||||
24
app/Libraries/ActivityPub/Activities/DeleteActivity.php
Normal file
24
app/Libraries/ActivityPub/Activities/DeleteActivity.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Activities;
|
||||
|
||||
use ActivityPub\Core\Activity;
|
||||
|
||||
class DeleteActivity extends Activity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Delete';
|
||||
}
|
||||
24
app/Libraries/ActivityPub/Activities/FollowActivity.php
Normal file
24
app/Libraries/ActivityPub/Activities/FollowActivity.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Activities;
|
||||
|
||||
use ActivityPub\Core\Activity;
|
||||
|
||||
class FollowActivity extends Activity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Follow';
|
||||
}
|
||||
24
app/Libraries/ActivityPub/Activities/LikeActivity.php
Normal file
24
app/Libraries/ActivityPub/Activities/LikeActivity.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Activities;
|
||||
|
||||
use ActivityPub\Core\Activity;
|
||||
|
||||
class LikeActivity extends Activity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Like';
|
||||
}
|
||||
24
app/Libraries/ActivityPub/Activities/UndoActivity.php
Normal file
24
app/Libraries/ActivityPub/Activities/UndoActivity.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Activities;
|
||||
|
||||
use ActivityPub\Core\Activity;
|
||||
|
||||
class UndoActivity extends Activity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Undo';
|
||||
}
|
||||
119
app/Libraries/ActivityPub/ActivityRequest.php
Normal file
119
app/Libraries/ActivityPub/ActivityRequest.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub;
|
||||
|
||||
use CodeIgniter\I18n\Time;
|
||||
use phpseclib\Crypt\RSA;
|
||||
|
||||
class ActivityRequest
|
||||
{
|
||||
/**
|
||||
* @var \CodeIgniter\HTTP\CURLRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var \CodeIgniter\HTTP\URI
|
||||
*/
|
||||
protected $uri;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Core\Activity|null
|
||||
*/
|
||||
protected $activity;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Accept' => 'application/activity+json', // TODO: outgoing and incoming requests
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @param string $activityPayload
|
||||
*/
|
||||
public function __construct($uri, $activityPayload = null)
|
||||
{
|
||||
$this->request = \Config\Services::curlrequest();
|
||||
|
||||
if ($activityPayload) {
|
||||
$this->request->setBody($activityPayload);
|
||||
}
|
||||
|
||||
$this->uri = new \CodeIgniter\HTTP\URI($uri);
|
||||
}
|
||||
|
||||
public function post()
|
||||
{
|
||||
// send Message to Fediverse instance
|
||||
$this->request->post($this->uri, $this->options);
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->request->get($this->uri, $this->options);
|
||||
}
|
||||
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->uri->getHost() .
|
||||
($this->uri->getPort() ? ':' . $this->uri->getPort() : '');
|
||||
}
|
||||
|
||||
public function sign($keyId, $privateKey)
|
||||
{
|
||||
$rsa = new RSA();
|
||||
$rsa->loadKey($privateKey); // private key
|
||||
$rsa->setHash('sha256');
|
||||
$rsa->setSignatureMode(RSA::SIGNATURE_PKCS1);
|
||||
|
||||
$path =
|
||||
$this->uri->getPath() .
|
||||
($this->uri->getQuery() ? "?{$this->uri->getQuery()}" : '');
|
||||
$host = $this->uri->getHost();
|
||||
$date = Time::now('GMT')->format('D, d M Y H:i:s T');
|
||||
$digest = 'SHA-256=' . base64_encode($this->getBodyDigest());
|
||||
$contentType = $this->options['headers']['Content-Type'];
|
||||
$contentLength = strval(strlen($this->request->getBody()));
|
||||
$userAgent = 'Castopod';
|
||||
|
||||
$plainText = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: $digest\ncontent-type: $contentType\ncontent-length: $contentLength\nuser-agent: $userAgent";
|
||||
|
||||
$signature = $rsa->sign($plainText);
|
||||
|
||||
$signatureHeader =
|
||||
'keyId="' .
|
||||
$keyId .
|
||||
'",algorithm="rsa-sha256",headers="(request-target) host date digest content-type content-length user-agent",signature="' .
|
||||
base64_encode($signature) .
|
||||
'"';
|
||||
|
||||
$this->options = [
|
||||
'headers' => [
|
||||
'Content-Type' => $contentType,
|
||||
'Content-Length' => $contentLength,
|
||||
'Authorization' => "Signature $signatureHeader",
|
||||
'Signature' => $signatureHeader,
|
||||
'Host' => $host,
|
||||
'Date' => $date,
|
||||
'User-Agent' => $userAgent,
|
||||
'Digest' => $digest,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getBodyDigest()
|
||||
{
|
||||
return hash('sha256', $this->request->getBody(), true);
|
||||
}
|
||||
}
|
||||
22
app/Libraries/ActivityPub/Config/ActivityPub.php
Normal file
22
app/Libraries/ActivityPub/Config/ActivityPub.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class ActivityPub extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* ActivityPub Objects
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
public $actorObject = 'ActivityPub\Objects\ActorObject';
|
||||
public $noteObject = 'ActivityPub\Objects\NoteObject';
|
||||
}
|
||||
106
app/Libraries/ActivityPub/Config/Routes.php
Normal file
106
app/Libraries/ActivityPub/Config/Routes.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
$routes->addPlaceholder('actorUsername', '[a-zA-Z0-9\_]{1,32}');
|
||||
$routes->addPlaceholder(
|
||||
'uuid',
|
||||
'[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}',
|
||||
);
|
||||
$routes->addPlaceholder('noteAction', '\bfavourite|\breblog|\breply');
|
||||
|
||||
/**
|
||||
* ActivityPub routes file
|
||||
*/
|
||||
|
||||
$routes->group('', ['namespace' => 'ActivityPub\Controllers'], function (
|
||||
$routes
|
||||
) {
|
||||
// webfinger
|
||||
$routes->get('.well-known/webfinger', 'WebFingerController', [
|
||||
'as' => 'webfinger',
|
||||
]);
|
||||
|
||||
// Actor
|
||||
$routes->group('@(:actorUsername)', function ($routes) {
|
||||
// Actor
|
||||
$routes->get('/', 'ActorController/$1', [
|
||||
'as' => 'actor',
|
||||
]);
|
||||
$routes->post('inbox', 'ActorController::inbox/$1', [
|
||||
'as' => 'inbox',
|
||||
'filter' =>
|
||||
'activity-pub:verify-activitystream,verify-blocks,verify-signature',
|
||||
]);
|
||||
$routes->get('outbox', 'ActorController::outbox/$1', [
|
||||
'as' => 'outbox',
|
||||
'filter' => 'activity-pub:verify-activitystream',
|
||||
]);
|
||||
$routes->get('followers', 'ActorController::followers/$1', [
|
||||
'as' => 'followers',
|
||||
'filter' => 'activity-pub::activity-stream',
|
||||
]);
|
||||
$routes->post('follow', 'ActorController::attemptFollow/$1', [
|
||||
'as' => 'attempt-follow',
|
||||
]);
|
||||
$routes->get('activities/(:uuid)', 'ActorController::activity/$1/$2', [
|
||||
'as' => 'activity',
|
||||
]);
|
||||
});
|
||||
|
||||
// Note
|
||||
$routes->post('notes/new', 'NoteController::attemptCreate/$1', [
|
||||
'as' => 'note-attempt-create',
|
||||
]);
|
||||
|
||||
$routes->get('notes/(:uuid)', 'NoteController/$1', [
|
||||
'as' => 'note',
|
||||
]);
|
||||
|
||||
$routes->get('notes/(:uuid)/replies', 'NoteController/$1', [
|
||||
'as' => 'note-replies',
|
||||
]);
|
||||
|
||||
$routes->post(
|
||||
'notes/(:uuid)/remote/(:noteAction)',
|
||||
'NoteController::attemptRemoteAction/$1/$2/$3',
|
||||
[
|
||||
'as' => 'note-attempt-remote-action',
|
||||
],
|
||||
);
|
||||
|
||||
// Blocking actors and domains
|
||||
$routes->post(
|
||||
'fediverse-block-actor',
|
||||
'BlockController::attemptBlockActor',
|
||||
['as' => 'fediverse-attempt-block-actor'],
|
||||
);
|
||||
|
||||
$routes->post(
|
||||
'fediverse-block-domain',
|
||||
'BlockController::attemptBlockDomain',
|
||||
['as' => 'fediverse-attempt-block-domain'],
|
||||
);
|
||||
|
||||
$routes->post(
|
||||
'fediverse-unblock-actor',
|
||||
'BlockController::attemptUnblockActor',
|
||||
[
|
||||
'as' => 'fediverse-attempt-unblock-actor',
|
||||
],
|
||||
);
|
||||
|
||||
$routes->post(
|
||||
'fediverse-unblock-domain',
|
||||
'BlockController::attemptUnblockDomain',
|
||||
[
|
||||
'as' => 'fediverse-attempt-unblock-domain',
|
||||
],
|
||||
);
|
||||
|
||||
$routes->cli('scheduled-activities', 'SchedulerController::activity');
|
||||
});
|
||||
376
app/Libraries/ActivityPub/Controllers/ActorController.php
Normal file
376
app/Libraries/ActivityPub/Controllers/ActorController.php
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use ActivityPub\Objects\OrderedCollectionObject;
|
||||
use ActivityPub\Objects\OrderedCollectionPage;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class ActorController extends Controller
|
||||
{
|
||||
protected $helpers = ['activitypub'];
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Actor
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Config\ActivityPub
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('ActivityPub');
|
||||
}
|
||||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (count($params) > 0) {
|
||||
if (
|
||||
!($this->actor = model('ActorModel')->getActorByUsername(
|
||||
$params[0],
|
||||
))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
unset($params[0]);
|
||||
|
||||
return $this->$method(...$params);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$actorObjectClass = $this->config->actorObject;
|
||||
$actorObject = new $actorObjectClass($this->actor);
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody($actorObject->toJSON());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming requests from fediverse servers
|
||||
*/
|
||||
public function inbox()
|
||||
{
|
||||
// get json body and parse it
|
||||
$payload = $this->request->getJSON();
|
||||
|
||||
// retrieve payload actor from database or create it if it doesn't exist
|
||||
$payloadActor = get_or_create_actor_from_uri($payload->actor);
|
||||
|
||||
// store activity to database
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
$payload->type,
|
||||
$payloadActor->id,
|
||||
$this->actor->id,
|
||||
null,
|
||||
json_encode($payload),
|
||||
);
|
||||
|
||||
// switch/case on activity type
|
||||
switch ($payload->type) {
|
||||
case 'Create':
|
||||
switch ($payload->object->type) {
|
||||
case 'Note':
|
||||
if (!$payload->object->inReplyTo) {
|
||||
return $this->response
|
||||
->setStatusCode(501)
|
||||
->setJSON([]);
|
||||
}
|
||||
|
||||
$replyToNote = model('NoteModel')->getNoteByUri(
|
||||
$payload->object->inReplyTo,
|
||||
);
|
||||
|
||||
// TODO: strip content from html to retrieve message
|
||||
// remove all html tags and reconstruct message with mentions?
|
||||
extract_text_from_html($payload->object->content);
|
||||
|
||||
$reply = new \ActivityPub\Entities\Note([
|
||||
'uri' => $payload->object->id,
|
||||
'actor_id' => $payloadActor->id,
|
||||
'in_reply_to_id' => $replyToNote->id,
|
||||
'message' => $payload->object->content,
|
||||
'published_at' => Time::parse(
|
||||
$payload->object->published,
|
||||
),
|
||||
]);
|
||||
|
||||
$noteId = model('NoteModel')->addReply(
|
||||
$reply,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'note_id' => service('uuid')
|
||||
->fromBytes($noteId)
|
||||
->getString(),
|
||||
]);
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([]);
|
||||
default:
|
||||
// return not handled undo error (501 = not implemented)
|
||||
return $this->response->setStatusCode(501)->setJSON([]);
|
||||
}
|
||||
break;
|
||||
case 'Delete':
|
||||
$noteToDelete = model('NoteModel')->getNoteByUri(
|
||||
$payload->object->id,
|
||||
);
|
||||
|
||||
model('NoteModel')->removeNote($noteToDelete, false);
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([]);
|
||||
case 'Follow':
|
||||
// add to followers table
|
||||
model('FollowModel')->addFollower(
|
||||
$payloadActor,
|
||||
$this->actor,
|
||||
false,
|
||||
);
|
||||
|
||||
// Automatically accept follow by returning accept activity
|
||||
accept_follow($this->actor, $payloadActor, $payload->id);
|
||||
|
||||
// TODO: return 202 (Accepted) followed!
|
||||
return $this->response->setStatusCode(202)->setJSON([]);
|
||||
|
||||
case 'Like':
|
||||
// get favourited note
|
||||
$note = model('NoteModel')->getNoteByUri($payload->object);
|
||||
|
||||
// Like side-effect
|
||||
model('FavouriteModel')->addFavourite(
|
||||
$payloadActor,
|
||||
$note,
|
||||
false,
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'note_id' => $note->id,
|
||||
]);
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([]);
|
||||
case 'Announce':
|
||||
$note = model('NoteModel')->getNoteByUri($payload->object);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'note_id' => $note->id,
|
||||
]);
|
||||
|
||||
model('NoteModel')->reblog($payloadActor, $note, false);
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([]);
|
||||
case 'Undo':
|
||||
// switch/case on the type of activity to undo
|
||||
switch ($payload->object->type) {
|
||||
case 'Follow':
|
||||
// revert side-effect by removing follow from database
|
||||
model('FollowModel')->removeFollower(
|
||||
$payloadActor,
|
||||
$this->actor,
|
||||
false,
|
||||
);
|
||||
|
||||
// TODO: undo has been accepted! (202 - Accepted)
|
||||
return $this->response->setStatusCode(202)->setJSON([]);
|
||||
case 'Like':
|
||||
$note = model('NoteModel')->getNoteByUri(
|
||||
$payload->object->object,
|
||||
);
|
||||
|
||||
// revert side-effect by removing favourite from database
|
||||
model('FavouriteModel')->removeFavourite(
|
||||
$payloadActor,
|
||||
$note,
|
||||
false,
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'note_id' => $note->id,
|
||||
]);
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([]);
|
||||
case 'Announce':
|
||||
$note = model('NoteModel')->getNoteByUri(
|
||||
$payload->object->object,
|
||||
);
|
||||
|
||||
$reblogNote = model('NoteModel')
|
||||
->where([
|
||||
'actor_id' => $payloadActor->id,
|
||||
'reblog_of_id' => service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
])
|
||||
->first();
|
||||
|
||||
model('NoteModel')->undoReblog($reblogNote, false);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'note_id' => $note->id,
|
||||
]);
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([]);
|
||||
default:
|
||||
// return not handled undo error (501 = not implemented)
|
||||
return $this->response->setStatusCode(501)->setJSON([]);
|
||||
}
|
||||
default:
|
||||
// return not handled activity error (501 = not implemented)
|
||||
return $this->response->setStatusCode(501)->setJSON([]);
|
||||
}
|
||||
}
|
||||
|
||||
public function outbox()
|
||||
{
|
||||
// get published activities by publication date
|
||||
$actorActivity = model('ActivityModel')
|
||||
->where('actor_id', $this->actor->id)
|
||||
->where('`created_at` <= NOW()', null, false)
|
||||
->orderBy('created_at', 'DESC');
|
||||
|
||||
$pageNumber = $this->request->getGet('page');
|
||||
|
||||
if (!isset($pageNumber)) {
|
||||
$actorActivity->paginate(12);
|
||||
$pager = $actorActivity->pager;
|
||||
$collection = new OrderedCollectionObject(null, $pager);
|
||||
} else {
|
||||
$paginatedActivity = $actorActivity->paginate(
|
||||
12,
|
||||
'default',
|
||||
$pageNumber,
|
||||
);
|
||||
$pager = $actorActivity->pager;
|
||||
$orderedItems = [];
|
||||
foreach ($paginatedActivity as $activity) {
|
||||
array_push($orderedItems, $activity->payload);
|
||||
}
|
||||
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody($collection->toJSON());
|
||||
}
|
||||
|
||||
public function followers()
|
||||
{
|
||||
// get followers for a specific actor
|
||||
$followers = model('ActorModel')
|
||||
->join(
|
||||
'activitypub_follows',
|
||||
'activitypub_follows.actor_id = id',
|
||||
'inner',
|
||||
)
|
||||
->where('activitypub_follows.target_actor_id', $this->actor->id)
|
||||
->orderBy('activitypub_follows.created_at', 'DESC');
|
||||
|
||||
$pageNumber = $this->request->getGet('page');
|
||||
|
||||
if (!isset($pageNumber)) {
|
||||
$followers->paginate(12);
|
||||
$pager = $followers->pager;
|
||||
$followersCollection = new OrderedCollectionObject(null, $pager);
|
||||
} else {
|
||||
$paginatedFollowers = $followers->paginate(
|
||||
12,
|
||||
'default',
|
||||
$pageNumber,
|
||||
);
|
||||
$pager = $followers->pager;
|
||||
|
||||
$orderedItems = [];
|
||||
foreach ($paginatedFollowers as $follower) {
|
||||
array_push($orderedItems, $follower->uri);
|
||||
}
|
||||
$followersCollection = new OrderedCollectionPage(
|
||||
$pager,
|
||||
$orderedItems,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody($followersCollection->toJSON());
|
||||
}
|
||||
|
||||
public function attemptFollow()
|
||||
{
|
||||
$rules = [
|
||||
'handle' =>
|
||||
'regex_match[/^@?(?P<username>[\w\.\-]+)@(?P<host>[\w\.\-]+)(?P<port>:[\d]+)?$/]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
helper('text');
|
||||
|
||||
// get webfinger data from actor
|
||||
// parse activityPub id to get actor and domain
|
||||
// check if actor and domain exist
|
||||
|
||||
try {
|
||||
if ($parts = split_handle($this->request->getPost('handle'))) {
|
||||
extract($parts);
|
||||
|
||||
$data = get_webfinger_data($username, $domain);
|
||||
}
|
||||
} catch (\CodeIgniter\HTTP\Exceptions\HTTPException $e) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('error', lang('ActivityPub.follow.accountNotFound'));
|
||||
}
|
||||
|
||||
$ostatusKey = array_search(
|
||||
'http://ostatus.org/schema/1.0/subscribe',
|
||||
array_column($data->links, 'rel'),
|
||||
);
|
||||
|
||||
if (!$ostatusKey) {
|
||||
// TODO: error, couldn't subscribe to activitypub account
|
||||
// The instance doesn't allow its users to follow others
|
||||
return $this->response->setJSON([]);
|
||||
}
|
||||
|
||||
return redirect()->to(
|
||||
str_replace(
|
||||
'{uri}',
|
||||
urlencode($this->actor->uri),
|
||||
$data->links[$ostatusKey]->template,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function activity($activityId)
|
||||
{
|
||||
if (
|
||||
!($activity = model('ActivityModel')->getActivityById($activityId))
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody(json_encode($activity->payload));
|
||||
}
|
||||
}
|
||||
105
app/Libraries/ActivityPub/Controllers/BlockController.php
Normal file
105
app/Libraries/ActivityPub/Controllers/BlockController.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class BlockController extends Controller
|
||||
{
|
||||
protected $helpers = ['activitypub'];
|
||||
|
||||
public function attemptBlockActor()
|
||||
{
|
||||
$rules = [
|
||||
'handle' => 'required',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$handle = $this->request->getPost('handle');
|
||||
|
||||
if ($parts = split_handle($handle)) {
|
||||
extract($parts);
|
||||
|
||||
if (!($actor = get_or_create_actor($username, $domain))) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('error', 'Actor not found.');
|
||||
}
|
||||
|
||||
model('ActorModel')->blockActor($actor->id);
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
function attemptBlockDomain()
|
||||
{
|
||||
$rules = [
|
||||
'domain' => 'required',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
model('BlockedDomainModel')->blockDomain(
|
||||
$this->request->getPost('domain'),
|
||||
);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
function attemptUnblockActor()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
model('ActorModel')->unblockActor($this->request->getPost('actor_id'));
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
function attemptUnblockDomain()
|
||||
{
|
||||
$rules = [
|
||||
'domain' => 'required',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
model('BlockedDomainModel')->unblockDomain(
|
||||
$this->request->getPost('domain'),
|
||||
);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
278
app/Libraries/ActivityPub/Controllers/NoteController.php
Normal file
278
app/Libraries/ActivityPub/Controllers/NoteController.php
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use ActivityPub\Config\ActivityPub;
|
||||
use ActivityPub\Objects\OrderedCollectionObject;
|
||||
use ActivityPub\Objects\OrderedCollectionPage;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class NoteController extends Controller
|
||||
{
|
||||
protected $helpers = ['activitypub'];
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Note|null
|
||||
*/
|
||||
protected $note;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Config\ActivityPub
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('ActivityPub');
|
||||
}
|
||||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (!($this->note = model('NoteModel')->getNoteById($params[0]))) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
unset($params[0]);
|
||||
|
||||
return $this->$method(...$params);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$noteObjectClass = $this->config->noteObject;
|
||||
$noteObject = new $noteObjectClass($this->note);
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody($noteObject->toJSON());
|
||||
}
|
||||
|
||||
public function replies()
|
||||
{
|
||||
// get note replies
|
||||
$noteReplies = model('NoteModel')
|
||||
->where(
|
||||
'in_reply_to_id',
|
||||
service('uuid')
|
||||
->fromString($this->note->id)
|
||||
->getBytes(),
|
||||
)
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->orderBy('published_at', 'ASC');
|
||||
|
||||
$pageNumber = $this->request->getGet('page');
|
||||
|
||||
if (!isset($pageNumber)) {
|
||||
$noteReplies->paginate(12);
|
||||
$pager = $noteReplies->pager;
|
||||
$collection = new OrderedCollectionObject(null, $pager);
|
||||
} else {
|
||||
$paginatedReplies = $noteReplies->paginate(
|
||||
12,
|
||||
'default',
|
||||
$pageNumber,
|
||||
);
|
||||
$pager = $noteReplies->pager;
|
||||
|
||||
$orderedItems = [];
|
||||
$noteObjectClass = $this->config->noteObject;
|
||||
foreach ($paginatedReplies as $reply) {
|
||||
$replyObject = new $noteObjectClass($reply);
|
||||
array_push($orderedItems, $replyObject->toJSON());
|
||||
}
|
||||
$collection = new OrderedCollectionPage($pager, $orderedItems);
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setContentType('application/activity+json')
|
||||
->setBody($collection->toJSON());
|
||||
}
|
||||
|
||||
public function attemptCreate()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
'message' => 'required|max_length[500]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$newNote = new \ActivityPub\Entities\Note([
|
||||
'actor_id' => $this->request->getPost('actor_id'),
|
||||
'message' => $this->request->getPost('message'),
|
||||
'published_at' => Time::now(),
|
||||
]);
|
||||
|
||||
if (!model('NoteModel')->addNote($newNote)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
// TODO: translate
|
||||
->with('error', 'Couldn\'t create Note');
|
||||
}
|
||||
|
||||
// Note without preview card has been successfully created
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptFavourite()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$actor = model('ActorModel')->getActorById(
|
||||
$this->request->getPost('actor_id'),
|
||||
);
|
||||
|
||||
model('FavouriteModel')->toggleFavourite($actor, $this->note->id);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReblog()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$actor = model('ActorModel')->getActorById(
|
||||
$this->request->getPost('actor_id'),
|
||||
);
|
||||
|
||||
model('NoteModel')->toggleReblog($actor, $this->note);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptReply()
|
||||
{
|
||||
$rules = [
|
||||
'actor_id' => 'required|is_natural_no_zero',
|
||||
'message' => 'required|max_length[500]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$newReplyNote = new \ActivityPub\Entities\Note([
|
||||
'actor_id' => $this->request->getPost('actor_id'),
|
||||
'in_reply_to_id' => $this->note->id,
|
||||
'message' => $this->request->getPost('message'),
|
||||
'published_at' => Time::now(),
|
||||
]);
|
||||
|
||||
if (!model('NoteModel')->addReply($newReplyNote)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
// TODO: translate
|
||||
->with('error', 'Couldn\'t create Reply');
|
||||
}
|
||||
|
||||
// Reply note without preview card has been successfully created
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptRemoteAction($action)
|
||||
{
|
||||
$rules = [
|
||||
'handle' =>
|
||||
'regex_match[/^@?(?P<username>[\w\.\-]+)@(?P<host>[\w\.\-]+)(?P<port>:[\d]+)?$/]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
helper('text');
|
||||
|
||||
// get webfinger data from actor
|
||||
// parse activityPub id to get actor and domain
|
||||
// check if actor and domain exist
|
||||
try {
|
||||
if ($parts = split_handle($this->request->getPost('handle'))) {
|
||||
extract($parts);
|
||||
|
||||
$data = get_webfinger_data($username, $domain);
|
||||
}
|
||||
} catch (\CodeIgniter\HTTP\Exceptions\HTTPException $e) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('error', lang('ActivityPub.follow.accountNotFound'));
|
||||
}
|
||||
|
||||
$ostatusKey = array_search(
|
||||
'http://ostatus.org/schema/1.0/subscribe',
|
||||
array_column($data->links, 'rel'),
|
||||
);
|
||||
|
||||
if (!$ostatusKey) {
|
||||
// TODO: error, couldn't remote favourite/share/reply to note
|
||||
// The instance doesn't allow its users remote actions on notes
|
||||
return $this->response->setJSON([]);
|
||||
}
|
||||
|
||||
return redirect()->to(
|
||||
str_replace(
|
||||
'{uri}',
|
||||
urlencode($this->note->uri),
|
||||
$data->links[$ostatusKey]->template,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function attemptBlockActor()
|
||||
{
|
||||
model('ActorModel')->blockActor($this->note->actor->id);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptBlockDomain()
|
||||
{
|
||||
model('BlockedDomainModel')->blockDomain($this->note->actor->domain);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function attemptDelete()
|
||||
{
|
||||
model('NoteModel', false)->removeNote($this->note);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class SchedulerController extends Controller
|
||||
{
|
||||
protected $helpers = ['activitypub'];
|
||||
|
||||
public function activity()
|
||||
{
|
||||
// retrieve scheduled activities from database
|
||||
$scheduledActivities = model('ActivityModel')->getScheduledActivities();
|
||||
|
||||
// Send activity to all followers
|
||||
foreach ($scheduledActivities as $scheduledActivity) {
|
||||
// send activity to all actor followers
|
||||
send_activity_to_followers(
|
||||
$scheduledActivity->actor,
|
||||
json_encode($scheduledActivity->payload),
|
||||
);
|
||||
|
||||
// set activity status to delivered
|
||||
model('ActivityModel')->update($scheduledActivity->id, [
|
||||
'status' => 'delivered',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Controllers;
|
||||
|
||||
use ActivityPub\WebFinger;
|
||||
use CodeIgniter\Controller;
|
||||
use Exception;
|
||||
|
||||
class WebFingerController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$webfinger = new WebFinger($this->request->getGet('resource'));
|
||||
} catch (Exception $e) {
|
||||
// return 404, actor not found
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
return $this->response->setJSON($webfinger->toArray());
|
||||
}
|
||||
}
|
||||
50
app/Libraries/ActivityPub/Core/AbstractObject.php
Normal file
50
app/Libraries/ActivityPub/Core/AbstractObject.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This class defines the Object which is the
|
||||
* primary base type for the Activity Streams vocabulary.
|
||||
*
|
||||
* Object is a reserved word in php, so the class is named ObjectType.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Core;
|
||||
|
||||
abstract class AbstractObject
|
||||
{
|
||||
public function set($property, $value)
|
||||
{
|
||||
$this->$property = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$objectVars = get_object_vars($this);
|
||||
$array = [];
|
||||
foreach ($objectVars as $key => $value) {
|
||||
if ($key === 'context') {
|
||||
$key = '@context';
|
||||
}
|
||||
if (is_object($value) && $value instanceof self) {
|
||||
$array[$key] = $value->toArray();
|
||||
} else {
|
||||
$array[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// removes all NULL, FALSE and Empty Strings but leaves 0 (zero) values
|
||||
return array_filter($array, function ($value) {
|
||||
return $value !== null && $value !== false && $value !== '';
|
||||
});
|
||||
}
|
||||
|
||||
public function toJSON()
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
32
app/Libraries/ActivityPub/Core/Activity.php
Normal file
32
app/Libraries/ActivityPub/Core/Activity.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Activity objects are specializations of the base Object type
|
||||
* that provide information about actions that have either
|
||||
* already occurred, are in the process of occurring,
|
||||
* or may occur in the future.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Core;
|
||||
|
||||
class Activity extends ObjectType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Activity';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* @var string|\ActivityPub\Core\ObjectType
|
||||
*/
|
||||
protected $object;
|
||||
}
|
||||
52
app/Libraries/ActivityPub/Core/ObjectType.php
Normal file
52
app/Libraries/ActivityPub/Core/ObjectType.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This class defines the Object which is the
|
||||
* primary base type for the Activity Streams vocabulary.
|
||||
*
|
||||
* Object is a reserved word in php, so the class is named ObjectType.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Core;
|
||||
|
||||
class ObjectType extends AbstractObject
|
||||
{
|
||||
/**
|
||||
* @var array|string
|
||||
*/
|
||||
protected $context = 'https://www.w3.org/ns/activitystreams';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Object';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $published;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $cc;
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddActors
|
||||
* Creates activitypub_actors table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddActors extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'uri' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
'username' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 32,
|
||||
],
|
||||
'domain' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 191,
|
||||
],
|
||||
'private_key' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'public_key' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'display_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 128,
|
||||
],
|
||||
'summary' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'avatar_image_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
|
||||
'avatar_image_mimetype' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 13,
|
||||
],
|
||||
'cover_image_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'cover_image_mimetype' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 13,
|
||||
'null' => true,
|
||||
],
|
||||
'inbox_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
'outbox_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'followers_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'followers_count' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'notes_count' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'is_blocked' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey('uri');
|
||||
$this->forge->addUniqueKey(['username', 'domain']);
|
||||
$this->forge->createTable('activitypub_actors');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_actors');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddNotes
|
||||
* Creates activitypub_notes table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddNotes extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
],
|
||||
'uri' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 191,
|
||||
],
|
||||
'actor_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
],
|
||||
'in_reply_to_id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
'null' => true,
|
||||
],
|
||||
'reblog_of_id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
'null' => true,
|
||||
],
|
||||
'message' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 500,
|
||||
'null' => true,
|
||||
],
|
||||
'message_html' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 600,
|
||||
'null' => true,
|
||||
],
|
||||
'favourites_count' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'reblogs_count' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'replies_count' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'default' => 0,
|
||||
],
|
||||
'published_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey('uri');
|
||||
// FIXME: an actor must reblog a note only once
|
||||
// $this->forge->addUniqueKey(['actor_id', 'reblog_of_id']);
|
||||
$this->forge->addForeignKey(
|
||||
'actor_id',
|
||||
'activitypub_actors',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'in_reply_to_id',
|
||||
'activitypub_notes',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'reblog_of_id',
|
||||
'activitypub_notes',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('activitypub_notes');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_notes');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddActivities
|
||||
* Creates activitypub_activities table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddActivities extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
],
|
||||
'actor_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
],
|
||||
'target_actor_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'note_id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
'null' => true,
|
||||
],
|
||||
'type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
],
|
||||
'payload' => [
|
||||
'type' => 'JSON',
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'ENUM',
|
||||
'constraint' => ['queued', 'delivered'],
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'scheduled_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addForeignKey(
|
||||
'actor_id',
|
||||
'activitypub_actors',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'target_actor_id',
|
||||
'activitypub_actors',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'note_id',
|
||||
'activitypub_notes',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('activitypub_activities');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_activities');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddFavourites
|
||||
* Creates activitypub_favourites table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddFavourites extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'actor_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
],
|
||||
'note_id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
],
|
||||
]);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addPrimaryKey(['actor_id', 'note_id']);
|
||||
$this->forge->addForeignKey(
|
||||
'actor_id',
|
||||
'activitypub_actors',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'note_id',
|
||||
'activitypub_notes',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('activitypub_favourites');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_favourites');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddFollowers
|
||||
* Creates activitypub_followers table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddFollowers extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'actor_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'comment' => 'Actor that is following',
|
||||
],
|
||||
'target_actor_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'comment' => 'Actor that is followed',
|
||||
],
|
||||
]);
|
||||
$this->forge->addField(
|
||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
|
||||
);
|
||||
$this->forge->addPrimaryKey(['actor_id', 'target_actor_id']);
|
||||
$this->forge->addForeignKey(
|
||||
'actor_id',
|
||||
'activitypub_actors',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'target_actor_id',
|
||||
'activitypub_actors',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('activitypub_follows');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_follows');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddPreviewCards
|
||||
* Creates activitypub_preview_cards table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddPreviewCards extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 512,
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 128,
|
||||
],
|
||||
'description' => ['type' => 'TEXT'],
|
||||
'type' => [
|
||||
'type' => 'ENUM',
|
||||
'constraint' => ['link', 'video', 'image', 'rich'],
|
||||
'default' => 'link',
|
||||
],
|
||||
'author_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 64,
|
||||
'null' => true,
|
||||
],
|
||||
'author_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'provider_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
'provider_url' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
'image' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
'html' => [
|
||||
'type' => 'TEXT',
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey('url');
|
||||
$this->forge->createTable('activitypub_preview_cards');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_preview_cards');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddNotePreviewCards
|
||||
* Creates activitypub_notes_preview_cards table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddNotesPreviewCards extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'note_id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
],
|
||||
'preview_card_id' => [
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addPrimaryKey(['note_id', 'preview_card_id']);
|
||||
$this->forge->addForeignKey(
|
||||
'note_id',
|
||||
'activitypub_notes',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->addForeignKey(
|
||||
'preview_card_id',
|
||||
'activitypub_preview_cards',
|
||||
'id',
|
||||
false,
|
||||
'CASCADE',
|
||||
);
|
||||
$this->forge->createTable('activitypub_notes_preview_cards');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_notes_preview_cards');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class AddBlockedDomains
|
||||
* Creates activitypub_blocked_domains table in database
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddBlockedDomains extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 191,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('name');
|
||||
$this->forge->createTable('activitypub_blocked_domains');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('activitypub_blocked_domains');
|
||||
}
|
||||
}
|
||||
99
app/Libraries/ActivityPub/Entities/Activity.php
Normal file
99
app/Libraries/ActivityPub/Entities/Activity.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use Michalsn\Uuid\UuidEntity;
|
||||
|
||||
class Activity extends UuidEntity
|
||||
{
|
||||
protected $uuids = ['id', 'note_id'];
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Actor
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Actor
|
||||
*/
|
||||
protected $target_actor;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Note
|
||||
*/
|
||||
protected $note;
|
||||
|
||||
protected $dates = ['scheduled_at', 'created_at'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'string',
|
||||
'actor_id' => 'integer',
|
||||
'target_actor_id' => '?integer',
|
||||
'note_id' => '?string',
|
||||
'type' => 'string',
|
||||
'payload' => 'json',
|
||||
'status' => '?string',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return \ActivityPub\Entities\Actor
|
||||
*/
|
||||
public function getActor()
|
||||
{
|
||||
if (empty($this->actor_id)) {
|
||||
throw new \RuntimeException(
|
||||
'Activity must have an actor_id before getting the actor.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->actor)) {
|
||||
$this->actor = model('ActorModel')->getActorById($this->actor_id);
|
||||
}
|
||||
|
||||
return $this->actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \ActivityPub\Entities\Actor
|
||||
*/
|
||||
public function getTargetActor()
|
||||
{
|
||||
if (empty($this->target_actor_id)) {
|
||||
throw new \RuntimeException(
|
||||
'Activity must have a target_actor_id before getting the target actor.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->target_actor)) {
|
||||
$this->target_actor = model('ActorModel')->getActorById(
|
||||
$this->target_actor_id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->target_actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \ActivityPub\Entities\Note
|
||||
*/
|
||||
public function getNote()
|
||||
{
|
||||
if (empty($this->note_id)) {
|
||||
throw new \RuntimeException(
|
||||
'Activity must have a note_id before getting note.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->note)) {
|
||||
$this->note = model('NoteModel')->getNoteById($this->note_id);
|
||||
}
|
||||
|
||||
return $this->note;
|
||||
}
|
||||
}
|
||||
84
app/Libraries/ActivityPub/Entities/Actor.php
Normal file
84
app/Libraries/ActivityPub/Entities/Actor.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use CodeIgniter\Entity;
|
||||
|
||||
class Actor extends Entity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $key_id;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Actor[]
|
||||
*/
|
||||
protected $followers;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $is_local;
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'uri' => 'string',
|
||||
'username' => 'string',
|
||||
'domain' => 'string',
|
||||
'display_name' => 'string',
|
||||
'summary' => '?string',
|
||||
'private_key' => '?string',
|
||||
'public_key' => '?string',
|
||||
'avatar_image_url' => 'string',
|
||||
'avatar_image_mimetype' => 'string',
|
||||
'cover_image_url' => '?string',
|
||||
'cover_image_mimetype' => '?string',
|
||||
'inbox_url' => 'string',
|
||||
'outbox_url' => '?string',
|
||||
'followers_url' => '?string',
|
||||
'followers_count' => 'integer',
|
||||
'notes_count' => 'integer',
|
||||
'is_blocked' => 'boolean',
|
||||
];
|
||||
|
||||
public function getKeyId()
|
||||
{
|
||||
return $this->uri . '#main-key';
|
||||
}
|
||||
|
||||
public function getIsLocal()
|
||||
{
|
||||
if (!$this->is_local) {
|
||||
$uri = current_url(true);
|
||||
|
||||
$this->is_local =
|
||||
$this->domain ===
|
||||
$uri->getHost() .
|
||||
($uri->getPort() ? ':' . $uri->getPort() : '');
|
||||
}
|
||||
|
||||
return $this->is_local;
|
||||
}
|
||||
|
||||
public function getFollowers()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Actor must be created before getting followers.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->followers)) {
|
||||
$this->followers = model('ActorModel')->getFollowers($this->id);
|
||||
}
|
||||
|
||||
return $this->followers;
|
||||
}
|
||||
}
|
||||
18
app/Libraries/ActivityPub/Entities/BlockedDomain.php
Normal file
18
app/Libraries/ActivityPub/Entities/BlockedDomain.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use CodeIgniter\Entity;
|
||||
|
||||
class BlockedDomain extends Entity
|
||||
{
|
||||
protected $casts = [
|
||||
'name' => 'string',
|
||||
];
|
||||
}
|
||||
21
app/Libraries/ActivityPub/Entities/Favourite.php
Normal file
21
app/Libraries/ActivityPub/Entities/Favourite.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use Michalsn\Uuid\UuidEntity;
|
||||
|
||||
class Favourite extends UuidEntity
|
||||
{
|
||||
protected $uuids = ['note_id'];
|
||||
|
||||
protected $casts = [
|
||||
'actor_id' => 'integer',
|
||||
'note_id' => 'integer',
|
||||
];
|
||||
}
|
||||
19
app/Libraries/ActivityPub/Entities/Follow.php
Normal file
19
app/Libraries/ActivityPub/Entities/Follow.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use CodeIgniter\Entity;
|
||||
|
||||
class Follow extends Entity
|
||||
{
|
||||
protected $casts = [
|
||||
'actor_id' => 'integer',
|
||||
'target_actor_id' => 'integer',
|
||||
];
|
||||
}
|
||||
200
app/Libraries/ActivityPub/Entities/Note.php
Normal file
200
app/Libraries/ActivityPub/Entities/Note.php
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use Michalsn\Uuid\UuidEntity;
|
||||
|
||||
class Note extends UuidEntity
|
||||
{
|
||||
protected $uuids = ['id', 'in_reply_to_id', 'reblog_of_id'];
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Actor
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $is_reply;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Note
|
||||
*/
|
||||
protected $reply_to_note;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $is_reblog;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Note
|
||||
*/
|
||||
protected $reblog_of_note;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\PreviewCard
|
||||
*/
|
||||
protected $preview_card;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Note[]
|
||||
*/
|
||||
protected $replies;
|
||||
|
||||
/**
|
||||
* @var \ActivityPub\Entities\Note[]
|
||||
*/
|
||||
protected $reblogs;
|
||||
|
||||
protected $dates = ['published_at', 'created_at'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'string',
|
||||
'uri' => 'string',
|
||||
'actor_id' => 'integer',
|
||||
'in_reply_to_id' => '?string',
|
||||
'reblog_of_id' => '?string',
|
||||
'message' => 'string',
|
||||
'message_html' => 'string',
|
||||
'favourites_count' => 'integer',
|
||||
'reblogs_count' => 'integer',
|
||||
'replies_count' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the note's actor
|
||||
*
|
||||
* @return \ActivityPub\Entities\Actor
|
||||
*/
|
||||
public function getActor()
|
||||
{
|
||||
if (empty($this->actor_id)) {
|
||||
throw new \RuntimeException(
|
||||
'Note must have an actor_id before getting actor.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->actor)) {
|
||||
$this->actor = model('ActorModel')->getActorById($this->actor_id);
|
||||
}
|
||||
|
||||
return $this->actor;
|
||||
}
|
||||
|
||||
public function getPreviewCard()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Note must be created before getting preview_card.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->preview_card)) {
|
||||
$this->preview_card = model('PreviewCardModel')->getNotePreviewCard(
|
||||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->preview_card;
|
||||
}
|
||||
|
||||
public function getReplies()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Note must be created before getting replies.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->replies)) {
|
||||
$this->replies = model('NoteModel')->getNoteReplies($this->id);
|
||||
}
|
||||
|
||||
return $this->replies;
|
||||
}
|
||||
|
||||
public function getIsReply()
|
||||
{
|
||||
$this->is_reply = $this->in_reply_to_id !== null;
|
||||
|
||||
return $this->is_reply;
|
||||
}
|
||||
|
||||
public function getReplyToNote()
|
||||
{
|
||||
if (empty($this->in_reply_to_id)) {
|
||||
throw new \RuntimeException('Note is not a reply.');
|
||||
}
|
||||
|
||||
if (empty($this->reply_to_note)) {
|
||||
$this->reply_to_note = model('NoteModel')->getNoteById(
|
||||
$this->in_reply_to_id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->reply_to_note;
|
||||
}
|
||||
|
||||
public function getReblogs()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Note must be created before getting reblogs.',
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->reblogs)) {
|
||||
$this->reblogs = model('NoteModel')->getNoteReblogs(
|
||||
service('uuid')
|
||||
->fromString($this->id)
|
||||
->getBytes(),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->reblogs;
|
||||
}
|
||||
|
||||
public function getIsReblog()
|
||||
{
|
||||
return $this->reblog_of_id != null;
|
||||
}
|
||||
|
||||
public function getReblogOfNote()
|
||||
{
|
||||
if (empty($this->reblog_of_id)) {
|
||||
throw new \RuntimeException('Note is not a reblog.');
|
||||
}
|
||||
|
||||
if (empty($this->reblog_of_note)) {
|
||||
$this->reblog_of_note = model('NoteModel')->getNoteById(
|
||||
$this->reblog_of_id,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->reblog_of_note;
|
||||
}
|
||||
|
||||
public function setMessage(string $message)
|
||||
{
|
||||
helper('activitypub');
|
||||
|
||||
$messageWithoutTags = strip_tags($message);
|
||||
|
||||
$this->attributes['message'] = $messageWithoutTags;
|
||||
$this->attributes['message_html'] = str_replace(
|
||||
"\n",
|
||||
'<br />',
|
||||
linkify($messageWithoutTags),
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
29
app/Libraries/ActivityPub/Entities/PreviewCard.php
Normal file
29
app/Libraries/ActivityPub/Entities/PreviewCard.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use CodeIgniter\Entity;
|
||||
|
||||
class PreviewCard extends Entity
|
||||
{
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'note_id' => 'string',
|
||||
'url' => 'string',
|
||||
'title' => 'string',
|
||||
'description' => 'string',
|
||||
'type' => 'string',
|
||||
'author_name' => '?string',
|
||||
'author_url' => '?string',
|
||||
'provider_name' => '?string',
|
||||
'provider_url' => '?string',
|
||||
'image' => '?string',
|
||||
'html' => '?string',
|
||||
];
|
||||
}
|
||||
99
app/Libraries/ActivityPub/Filters/ActivityPubFilter.php
Normal file
99
app/Libraries/ActivityPub/Filters/ActivityPubFilter.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace ActivityPub\Filters;
|
||||
|
||||
use ActivityPub\HttpSignature;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
|
||||
class ActivityPubFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* Do whatever processing this filter needs to do.
|
||||
* By default it should not return anything during
|
||||
* normal execution. However, when an abnormal state
|
||||
* is found, it should return an instance of
|
||||
* CodeIgniter\HTTP\Response. If it does, script
|
||||
* execution will end and that Response will be
|
||||
* sent back to the client, allowing for error pages,
|
||||
* redirects, etc.
|
||||
*
|
||||
* @param \CodeIgniter\HTTP\RequestInterface $request
|
||||
* @param array|null $params
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function before(RequestInterface $request, $params = null)
|
||||
{
|
||||
if (empty($params)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array('verify-activitystream', $params)) {
|
||||
$negotiate = \Config\Services::negotiator();
|
||||
|
||||
$allowedContentTypes = [
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams',
|
||||
'application/activity+json',
|
||||
];
|
||||
|
||||
if (empty($negotiate->media($allowedContentTypes))) {
|
||||
// return $this->response->setStatusCode(415)->setJSON([]);
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('verify-blocks', $params)) {
|
||||
$payload = $request->getJSON();
|
||||
|
||||
$actorUri = $payload->actor;
|
||||
$domain = (new URI($actorUri))->getHost();
|
||||
|
||||
// check first if domain is blocked
|
||||
if (model('BlockedDomainModel')->isDomainBlocked($domain)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
// check if actor is blocked
|
||||
if (model('ActorModel')->isActorBlocked($actorUri)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('verify-signature', $params)) {
|
||||
try {
|
||||
// securityCheck: check activity signature before handling it
|
||||
(new HttpSignature())->verify();
|
||||
} catch (\Exception $e) {
|
||||
// Invalid HttpSignature (401 = unauthorized)
|
||||
// TODO: show error message?
|
||||
return service('response')->setStatusCode(401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Allows After filters to inspect and modify the response
|
||||
* object as needed. This method does not allow any way
|
||||
* to stop execution of other after filters, short of
|
||||
* throwing an Exception or Error.
|
||||
*
|
||||
* @param \CodeIgniter\HTTP\RequestInterface $request
|
||||
* @param \CodeIgniter\HTTP\ResponseInterface $response
|
||||
* @param array|null $arguments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function after(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
$arguments = null
|
||||
) {
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
}
|
||||
513
app/Libraries/ActivityPub/Helpers/activitypub_helper.php
Normal file
513
app/Libraries/ActivityPub/Helpers/activitypub_helper.php
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
use ActivityPub\Activities\AcceptActivity;
|
||||
use ActivityPub\ActivityRequest;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
|
||||
if (!function_exists('get_webfinger_data')) {
|
||||
/**
|
||||
* Retrieve actor webfinger data from username and domain
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $domain
|
||||
* @return mixed
|
||||
* @throws HTTPException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
function get_webfinger_data($username, $domain)
|
||||
{
|
||||
$webfingerUri = new \CodeIgniter\HTTP\URI();
|
||||
$webfingerUri->setScheme('https');
|
||||
$webfingerUri->setHost($domain);
|
||||
isset($port) && $webfingerUri->setPort((int) $port);
|
||||
$webfingerUri->setPath('/.well-known/webfinger');
|
||||
$webfingerUri->setQuery("resource=acct:{$username}@{$domain}");
|
||||
|
||||
$webfingerRequest = new ActivityRequest($webfingerUri);
|
||||
$webfingerResponse = $webfingerRequest->get();
|
||||
|
||||
return json_decode($webfingerResponse->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('split_handle')) {
|
||||
/**
|
||||
* Splits handle into its parts (username, host and port)
|
||||
*
|
||||
* @param string $handle
|
||||
* @return bool|array
|
||||
*/
|
||||
function split_handle(string $handle)
|
||||
{
|
||||
if (
|
||||
!preg_match(
|
||||
'/^@?(?P<username>[\w\.\-]+)@(?P<domain>[\w\.\-]+)(?P<port>:[\d]+)?$/',
|
||||
$handle,
|
||||
$matches,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('accept_follow')) {
|
||||
/**
|
||||
* Sends an accept activity to the targetActor's inbox
|
||||
*
|
||||
* @param \ActivityPub\Entities\Actor $actor Actor which accepts the follow
|
||||
* @param \ActivityPub\Entities\Actor $targetActor Actor which receives the accept follow
|
||||
* @param string $objectId
|
||||
* @return void
|
||||
*/
|
||||
function accept_follow($actor, $targetActor, $objectId)
|
||||
{
|
||||
$acceptActivity = new AcceptActivity();
|
||||
$acceptActivity->set('actor', $actor->uri)->set('object', $objectId);
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
|
||||
$activityModel = model('ActivityModel');
|
||||
$activityId = $activityModel->newActivity(
|
||||
'Accept',
|
||||
$actor->id,
|
||||
$targetActor->id,
|
||||
null,
|
||||
$acceptActivity->toJSON(),
|
||||
);
|
||||
|
||||
$acceptActivity->set(
|
||||
'id',
|
||||
url_to('activity', $actor->username, $activityId),
|
||||
);
|
||||
|
||||
$activityModel->update($activityId, [
|
||||
'payload' => $acceptActivity->toJSON(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$acceptRequest = new ActivityRequest(
|
||||
$targetActor->inbox_url,
|
||||
$acceptActivity->toJSON(),
|
||||
);
|
||||
$acceptRequest->sign($actor->key_id, $actor->private_key);
|
||||
$acceptRequest->post();
|
||||
} catch (\Exception $e) {
|
||||
$db->transRollback();
|
||||
}
|
||||
|
||||
$db->transComplete();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('send_activity_to_followers')) {
|
||||
/**
|
||||
* Sends an activity to all actor followers
|
||||
*
|
||||
* @param \ActivityPub\Entities\Actor $actor
|
||||
* @param string $activity
|
||||
* @return void
|
||||
*/
|
||||
function send_activity_to_followers($actor, $activityPayload)
|
||||
{
|
||||
foreach ($actor->followers as $follower) {
|
||||
try {
|
||||
$acceptRequest = new ActivityRequest(
|
||||
$follower->inbox_url,
|
||||
$activityPayload,
|
||||
);
|
||||
$acceptRequest->sign($actor->key_id, $actor->private_key);
|
||||
$acceptRequest->post();
|
||||
} catch (\Exception $e) {
|
||||
// log error
|
||||
log_message('critical', $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('extract_urls_from_message')) {
|
||||
/**
|
||||
* Returns an array of all urls from a string
|
||||
*
|
||||
* @param mixed $message
|
||||
* @return string[]
|
||||
*/
|
||||
function extract_urls_from_message($message)
|
||||
{
|
||||
preg_match_all(
|
||||
'~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i',
|
||||
$message,
|
||||
$match,
|
||||
);
|
||||
|
||||
return $match[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('create_preview_card_from_url')) {
|
||||
/**
|
||||
* Extract open graph metadata from given url and create preview card
|
||||
*
|
||||
* @param \CodeIgniter\HTTP\URI $url
|
||||
* @return \ActivityPub\Entities\PreviewCard|null
|
||||
*/
|
||||
function create_preview_card_from_url($url)
|
||||
{
|
||||
$essence = new \Essence\Essence([
|
||||
'filters' => [
|
||||
'OEmbedProvider' => '//',
|
||||
'OpenGraphProvider' => '//',
|
||||
'TwitterCardsProvider' => '//',
|
||||
],
|
||||
]);
|
||||
$media = $essence->extract((string) $url);
|
||||
|
||||
if ($media) {
|
||||
$typeMapping = [
|
||||
'photo' => 'image',
|
||||
'video' => 'video',
|
||||
'website' => 'link',
|
||||
'rich' => 'rich',
|
||||
];
|
||||
|
||||
// Check that, at least, the url and title are set
|
||||
if ($media->url && $media->title) {
|
||||
$preview_card = new \ActivityPub\Entities\PreviewCard([
|
||||
'url' => (string) $url,
|
||||
'title' => $media->title,
|
||||
'description' => $media->description,
|
||||
'type' => isset($typeMapping[$media->type])
|
||||
? $typeMapping[$media->type]
|
||||
: 'link',
|
||||
'author_name' => $media->authorName,
|
||||
'author_url' => $media->authorUrl,
|
||||
'provider_name' => $media->providerName,
|
||||
'provider_url' => $media->providerUrl,
|
||||
'image' => $media->thumbnailUrl,
|
||||
'html' => $media->html,
|
||||
]);
|
||||
|
||||
if (
|
||||
!($newPreviewCardId = model('PreviewCardModel')->insert(
|
||||
$preview_card,
|
||||
true,
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$preview_card->id = $newPreviewCardId;
|
||||
return $preview_card;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_or_create_preview_card_from_url')) {
|
||||
/**
|
||||
* Extract open graph metadata from given url and create preview card
|
||||
*
|
||||
* @param \CodeIgniter\HTTP\URI $url
|
||||
* @return \ActivityPub\Entities\PreviewCard|null
|
||||
*/
|
||||
function get_or_create_preview_card_from_url($url)
|
||||
{
|
||||
// check if preview card has already been generated
|
||||
if (
|
||||
$previewCard = model('PreviewCardModel')->getPreviewCardFromUrl(
|
||||
(string) $url,
|
||||
)
|
||||
) {
|
||||
return $previewCard;
|
||||
}
|
||||
|
||||
// create preview card
|
||||
return create_preview_card_from_url($url);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_or_create_actor_from_uri')) {
|
||||
/**
|
||||
* Retrieves actor from database using the actor uri
|
||||
* If Actor is not present, it creates the record in the database and returns it.
|
||||
*
|
||||
* @param string $actorUri
|
||||
* @return \ActivityPub\Entities\Actor|null
|
||||
*/
|
||||
function get_or_create_actor_from_uri($actorUri)
|
||||
{
|
||||
// check if actor exists in database already and return it
|
||||
if ($actor = model('ActorModel')->getActorByUri($actorUri)) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
// if the actor doesn't exist, request actorUri to create it
|
||||
return create_actor_from_uri($actorUri);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_or_create_actor')) {
|
||||
/**
|
||||
* Retrieves actor from database using the actor username and domain
|
||||
* If actor is not present, it creates the record in the database and returns it.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $domain
|
||||
* @return \ActivityPub\Entities\Actor|null
|
||||
*/
|
||||
function get_or_create_actor($username, $domain)
|
||||
{
|
||||
// check if actor exists in database already and return it
|
||||
if (
|
||||
$actor = model('ActorModel')->getActorByUsername($username, $domain)
|
||||
) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
// get actorUri with webfinger request
|
||||
$webfingerData = get_webfinger_data($username, $domain);
|
||||
$actorUriKey = array_search(
|
||||
'self',
|
||||
array_column($webfingerData->links, 'rel'),
|
||||
);
|
||||
|
||||
return create_actor_from_uri($webfingerData->links[$actorUriKey]->href);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('create_actor_from_uri')) {
|
||||
/**
|
||||
* Creates actor record in database using
|
||||
* the info gathered from the actorUri parameter
|
||||
*
|
||||
* @param string $actorUri
|
||||
* @return \ActivityPub\Entities\Actor|null
|
||||
*/
|
||||
function create_actor_from_uri($actorUri)
|
||||
{
|
||||
$activityRequest = new ActivityRequest($actorUri);
|
||||
$actorResponse = $activityRequest->get();
|
||||
$actorPayload = json_decode($actorResponse->getBody());
|
||||
|
||||
$newActor = new \ActivityPub\Entities\Actor();
|
||||
$newActor->uri = $actorUri;
|
||||
$newActor->username = $actorPayload->preferredUsername;
|
||||
$newActor->domain = $activityRequest->getDomain();
|
||||
$newActor->public_key = $actorPayload->publicKey->publicKeyPem;
|
||||
$newActor->private_key = null;
|
||||
$newActor->display_name = $actorPayload->name;
|
||||
$newActor->summary = $actorPayload->summary;
|
||||
if (property_exists($actorPayload, 'icon')) {
|
||||
$newActor->avatar_image_url = $actorPayload->icon->url;
|
||||
$newActor->avatar_image_mimetype = $actorPayload->icon->mediaType;
|
||||
}
|
||||
|
||||
if (property_exists($actorPayload, 'image')) {
|
||||
$newActor->cover_image_url = $actorPayload->image->url;
|
||||
$newActor->cover_image_mimetype = $actorPayload->image->mediaType;
|
||||
}
|
||||
$newActor->inbox_url = $actorPayload->inbox;
|
||||
$newActor->outbox_url = $actorPayload->outbox;
|
||||
$newActor->followers_url = $actorPayload->followers;
|
||||
|
||||
if (!($newActorId = model('ActorModel')->insert($newActor, true))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newActor->id = $newActorId;
|
||||
return $newActor;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_current_domain')) {
|
||||
/**
|
||||
* Returns instance's domain name
|
||||
*
|
||||
* @return string
|
||||
* @throws HTTPException
|
||||
*/
|
||||
function get_current_domain()
|
||||
{
|
||||
$uri = current_url(true);
|
||||
return $uri->getHost() . ($uri->getPort() ? ':' . $uri->getPort() : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('extract_text_from_html')) {
|
||||
/**
|
||||
* Extracts the text from html content
|
||||
*
|
||||
* @param mixed $content
|
||||
* @return string|string[]|null
|
||||
*/
|
||||
function extract_text_from_html($content)
|
||||
{
|
||||
return preg_replace('/\s+/', ' ', strip_tags($content));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('linkify')) {
|
||||
/**
|
||||
* Turn all link elements in clickable links.
|
||||
* Transforms urls and handles
|
||||
*
|
||||
* @param string $value
|
||||
* @param array $protocols http/https, ftp, mail, twitter
|
||||
* @param array $attributes
|
||||
* @return string
|
||||
*/
|
||||
function linkify($text, $protocols = ['http', 'handle'])
|
||||
{
|
||||
$links = [];
|
||||
|
||||
// Extract text links for each protocol
|
||||
foreach ((array) $protocols as $protocol) {
|
||||
switch ($protocol) {
|
||||
case 'http':
|
||||
case 'https':
|
||||
$text = preg_replace_callback(
|
||||
'~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i',
|
||||
function ($match) use ($protocol, &$links) {
|
||||
if ($match[1]) {
|
||||
$protocol = $match[1];
|
||||
}
|
||||
$link = $match[2] ?: $match[3];
|
||||
|
||||
helper('text');
|
||||
|
||||
$link = preg_replace(
|
||||
'#^www\.(.+\.)#i',
|
||||
'$1',
|
||||
$link,
|
||||
);
|
||||
|
||||
return '<' .
|
||||
array_push(
|
||||
$links,
|
||||
anchor(
|
||||
"$protocol://$link",
|
||||
ellipsize(rtrim($link, '/'), 30),
|
||||
[
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
],
|
||||
),
|
||||
) .
|
||||
'>';
|
||||
},
|
||||
$text,
|
||||
);
|
||||
break;
|
||||
case 'handle':
|
||||
$text = preg_replace_callback(
|
||||
'~(?<!\w)@(?<username>\w++)(?:@(?<domain>(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]))?~',
|
||||
function ($match) use (&$links) {
|
||||
// check if host is set and look for actor in database
|
||||
if (isset($match['host'])) {
|
||||
if (
|
||||
$actor = model(
|
||||
'ActorModel',
|
||||
)->getActorByUsername(
|
||||
$match['username'],
|
||||
$match['domain'],
|
||||
)
|
||||
) {
|
||||
// TODO: check that host is local to remove target blank?
|
||||
return '<' .
|
||||
array_push(
|
||||
$links,
|
||||
anchor($actor->uri, $match[0], [
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
]),
|
||||
) .
|
||||
'>';
|
||||
} else {
|
||||
try {
|
||||
$actor = get_or_create_actor(
|
||||
$match['username'],
|
||||
$match['domain'],
|
||||
);
|
||||
return '<' .
|
||||
array_push(
|
||||
$links,
|
||||
anchor($actor->uri, $match[0], [
|
||||
'target' => '_blank',
|
||||
'rel' =>
|
||||
'noopener noreferrer',
|
||||
]),
|
||||
) .
|
||||
'>';
|
||||
} catch (\CodeIgniter\HTTP\Exceptions\HTTPException $e) {
|
||||
// Couldn't retrieve actor, do not wrap the text in link
|
||||
return '<' .
|
||||
array_push($links, $match[0]) .
|
||||
'>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
$actor = model(
|
||||
'ActorModel',
|
||||
)->getActorByUsername($match['username'])
|
||||
) {
|
||||
return '<' .
|
||||
array_push(
|
||||
$links,
|
||||
anchor($actor->uri, $match[0]),
|
||||
) .
|
||||
'>';
|
||||
}
|
||||
|
||||
return '<' .
|
||||
array_push($links, $match[0]) .
|
||||
'>';
|
||||
}
|
||||
},
|
||||
$text,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
$text = preg_replace_callback(
|
||||
'~' .
|
||||
preg_quote($protocol, '~') .
|
||||
'://([^\s<]+?)(?<![\.,:])~i',
|
||||
function ($match) use ($protocol, &$links) {
|
||||
return '<' .
|
||||
array_push(
|
||||
$links,
|
||||
anchor("$protocol://$match[1]", $match[1], [
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
]),
|
||||
) .
|
||||
'>';
|
||||
},
|
||||
$text,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all links
|
||||
return preg_replace_callback(
|
||||
'/<(\d+)>/',
|
||||
function ($match) use (&$links) {
|
||||
return $links[$match[1] - 1];
|
||||
},
|
||||
$text,
|
||||
);
|
||||
}
|
||||
}
|
||||
170
app/Libraries/ActivityPub/HttpSignature.php
Normal file
170
app/Libraries/ActivityPub/HttpSignature.php
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is based on the HttpSignature file from the ActivityPhp package.
|
||||
* It is adapted to work with CodeIgniter4
|
||||
*
|
||||
* More info: https://github.com/landrok/activitypub
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub;
|
||||
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Services;
|
||||
use Exception;
|
||||
use phpseclib\Crypt\RSA;
|
||||
|
||||
/**
|
||||
* HTTP signatures tool
|
||||
*/
|
||||
class HttpSignature
|
||||
{
|
||||
const SIGNATURE_PATTERN = '/^
|
||||
keyId="(?P<keyId>
|
||||
(https?:\/\/[\w\-\.]+[\w]+)
|
||||
(:[\d]+)?
|
||||
([\w\-\.#\/@]+)
|
||||
)",
|
||||
algorithm="(?P<algorithm>[\w\-]+)",
|
||||
(headers="\(request-target\) (?P<headers>[\w\-\s]+)",)?
|
||||
signature="(?P<signature>[\w+\/]+={0,2})"
|
||||
/x';
|
||||
|
||||
/**
|
||||
* @var \CodeIgniter\HTTP\IncomingRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @param \CodeIgniter\HTTP\IncomingRequest $request
|
||||
*/
|
||||
public function __construct(IncomingRequest $request = null)
|
||||
{
|
||||
if (is_null($request)) {
|
||||
$request = Services::request();
|
||||
}
|
||||
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify an incoming message based upon its HTTP signature
|
||||
*
|
||||
* @return bool True if signature has been verified. Otherwise false
|
||||
*/
|
||||
public function verify()
|
||||
{
|
||||
if (!($dateHeader = $this->request->header('date'))) {
|
||||
throw new Exception('Request must include a date header.');
|
||||
}
|
||||
|
||||
// verify that request has been made within the last hour
|
||||
$currentTime = Time::now();
|
||||
$requestTime = Time::createFromFormat(
|
||||
'D, d M Y H:i:s T',
|
||||
$dateHeader->getValue(),
|
||||
);
|
||||
|
||||
$diff = $requestTime->difference($currentTime);
|
||||
if ($diff->getSeconds() > 3600) {
|
||||
throw new Exception('Request must be made within the last hour.');
|
||||
}
|
||||
|
||||
// check that digest header is set
|
||||
if (!($digestHeader = $this->request->header('digest'))) {
|
||||
throw new Exception('Request must include a digest header');
|
||||
}
|
||||
// compute body digest and compare with header digest
|
||||
$bodyDigest = hash('sha256', $this->request->getBody(), true);
|
||||
$digest = 'SHA-256=' . base64_encode($bodyDigest);
|
||||
if ($digest !== $digestHeader->getValue()) {
|
||||
throw new Exception('Request digest is incorrect.');
|
||||
}
|
||||
|
||||
// read the Signature header
|
||||
if (!($signature = $this->request->getHeaderLine('signature'))) {
|
||||
// Signature header not found
|
||||
throw new Exception('Request must include a signature header');
|
||||
}
|
||||
|
||||
// Split it into its parts (keyId, headers and signature)
|
||||
if (!($parts = $this->splitSignature($signature))) {
|
||||
throw new Exception('Malformed signature string.');
|
||||
}
|
||||
|
||||
// extract parts as $keyId, $headers and $signature variables
|
||||
extract($parts);
|
||||
|
||||
// Fetch the public key linked from keyId
|
||||
$actorRequest = new ActivityRequest($keyId);
|
||||
$actorResponse = $actorRequest->get();
|
||||
$actor = json_decode($actorResponse->getBody());
|
||||
|
||||
$publicKeyPem = $actor->publicKey->publicKeyPem;
|
||||
|
||||
// Create a comparison string from the plaintext headers we got
|
||||
// in the same order as was given in the signature header,
|
||||
$data = $this->getPlainText(explode(' ', trim($headers)));
|
||||
|
||||
// Verify that string using the public key and the original signature.
|
||||
$rsa = new RSA();
|
||||
$rsa->setHash('sha256');
|
||||
$rsa->setSignatureMode(RSA::SIGNATURE_PKCS1);
|
||||
$rsa->loadKey($publicKeyPem);
|
||||
|
||||
return $rsa->verify($data, base64_decode($signature, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split HTTP signature into its parts (keyId, headers and signature)
|
||||
*
|
||||
* @param string $signature
|
||||
* @return bool|array
|
||||
*/
|
||||
private function splitSignature(string $signature)
|
||||
{
|
||||
if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) {
|
||||
// Signature pattern failed
|
||||
return false;
|
||||
}
|
||||
|
||||
// Headers are optional
|
||||
if (!isset($matches['headers']) || $matches['headers'] == '') {
|
||||
$matches['headers'] = 'date';
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plain text that has been originally signed
|
||||
*
|
||||
* @param array $headers HTTP header keys
|
||||
* @return string
|
||||
*/
|
||||
private function getPlainText(array $headers)
|
||||
{
|
||||
$strings = [];
|
||||
$strings[] = sprintf(
|
||||
'(request-target): %s %s%s',
|
||||
$this->request->getMethod(),
|
||||
'/' . $this->request->uri->getPath(),
|
||||
$this->request->uri->getQuery()
|
||||
? '?' . $this->request->uri->getQuery()
|
||||
: '',
|
||||
);
|
||||
|
||||
foreach ($headers as $key) {
|
||||
if ($this->request->hasHeader($key)) {
|
||||
$strings[] = "$key: {$this->request->getHeaderLine($key)}";
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $strings);
|
||||
}
|
||||
}
|
||||
83
app/Libraries/ActivityPub/Models/ActivityModel.php
Normal file
83
app/Libraries/ActivityPub/Models/ActivityModel.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
class ActivityModel extends UuidModel
|
||||
{
|
||||
protected $table = 'activitypub_activities';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $uuidFields = ['id', 'note_id'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'actor_id',
|
||||
'target_actor_id',
|
||||
'note_id',
|
||||
'type',
|
||||
'payload',
|
||||
'status',
|
||||
'scheduled_at',
|
||||
];
|
||||
|
||||
protected $returnType = \ActivityPub\Entities\Activity::class;
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $updatedField = null;
|
||||
|
||||
public function getActivityById($activityId)
|
||||
{
|
||||
return $this->find($activityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new activity record in the database
|
||||
*
|
||||
* @param string $type
|
||||
* @param integer $actorId
|
||||
* @param integer $targetActorId
|
||||
* @param integer $noteId
|
||||
* @param string $payload
|
||||
* @param \CodeIgniter\I18n\Time $scheduledAt
|
||||
* @param string $status
|
||||
*
|
||||
* @return Michalsn\Uuid\BaseResult|int|string|false
|
||||
*/
|
||||
public function newActivity(
|
||||
$type,
|
||||
$actorId,
|
||||
$targetActorId,
|
||||
$noteId,
|
||||
$payload,
|
||||
$scheduledAt = null,
|
||||
$status = null
|
||||
) {
|
||||
return $this->insert(
|
||||
[
|
||||
'actor_id' => $actorId,
|
||||
'target_actor_id' => $targetActorId,
|
||||
'note_id' => $noteId,
|
||||
'type' => $type,
|
||||
'payload' => $payload,
|
||||
'scheduled_at' => $scheduledAt,
|
||||
'status' => $status,
|
||||
],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
public function getScheduledActivities()
|
||||
{
|
||||
return $this->where('`scheduled_at` <= NOW()', null, false)
|
||||
->where('status', 'queued')
|
||||
->orderBy('scheduled_at', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
125
app/Libraries/ActivityPub/Models/ActorModel.php
Normal file
125
app/Libraries/ActivityPub/Models/ActorModel.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ActorModel extends Model
|
||||
{
|
||||
protected $table = 'activitypub_actors';
|
||||
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'uri',
|
||||
'username',
|
||||
'domain',
|
||||
'display_name',
|
||||
'summary',
|
||||
'private_key',
|
||||
'public_key',
|
||||
'avatar_image_url',
|
||||
'avatar_image_mimetype',
|
||||
'cover_image_url',
|
||||
'cover_image_mimetype',
|
||||
'inbox_url',
|
||||
'outbox_url',
|
||||
'followers_url',
|
||||
'followers_count',
|
||||
'notes_count',
|
||||
'is_blocked',
|
||||
];
|
||||
|
||||
protected $returnType = \ActivityPub\Entities\Actor::class;
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
|
||||
public function getActorById($id)
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for actor with username and domain,
|
||||
* if no domain has been specified, the current host will be used
|
||||
*
|
||||
* @param mixed $username
|
||||
* @param mixed|null $domain
|
||||
* @return mixed
|
||||
*/
|
||||
public function getActorByUsername($username, $domain = null)
|
||||
{
|
||||
// TODO: is there a better way?
|
||||
helper('activitypub');
|
||||
|
||||
if (!$domain) {
|
||||
$domain = get_current_domain();
|
||||
}
|
||||
|
||||
if (!($found = cache("actor@{$username}@{$domain}"))) {
|
||||
$found = $this->where([
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
])->first();
|
||||
|
||||
cache()->save("actor@{$username}@{$domain}", $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getActorByUri($actorUri)
|
||||
{
|
||||
return $this->where('uri', $actorUri)->first();
|
||||
}
|
||||
|
||||
public function getFollowers($actorId)
|
||||
{
|
||||
return $this->join(
|
||||
'activitypub_follows',
|
||||
'activitypub_follows.actor_id = id',
|
||||
'inner',
|
||||
)
|
||||
->where('activitypub_follows.target_actor_id', $actorId)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an actor is blocked using its uri
|
||||
*
|
||||
* @param mixed $actorUri
|
||||
* @return boolean
|
||||
*/
|
||||
public function isActorBlocked($actorUri)
|
||||
{
|
||||
return $this->where(['uri' => $actorUri, 'is_blocked' => true])->first()
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all blocked actors.
|
||||
*
|
||||
* @return \ActivityPub\Entities\Actor[]
|
||||
*/
|
||||
public function getBlockedActors()
|
||||
{
|
||||
return $this->where('is_blocked', 1)->findAll();
|
||||
}
|
||||
|
||||
public function blockActor($actorId)
|
||||
{
|
||||
$this->update($actorId, ['is_blocked' => 1]);
|
||||
}
|
||||
|
||||
public function unblockActor($actorId)
|
||||
{
|
||||
$this->update($actorId, ['is_blocked' => 0]);
|
||||
}
|
||||
}
|
||||
79
app/Libraries/ActivityPub/Models/BlockedDomainModel.php
Normal file
79
app/Libraries/ActivityPub/Models/BlockedDomainModel.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class BlockedDomainModel extends Model
|
||||
{
|
||||
protected $table = 'activitypub_blocked_domains';
|
||||
protected $primaryKey = 'name';
|
||||
|
||||
protected $allowedFields = ['name'];
|
||||
|
||||
protected $returnType = \ActivityPub\Entities\BlockedDomain::class;
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $updatedField = null;
|
||||
|
||||
/**
|
||||
* Retrieves instance or podcast domain blocks depending on whether or not $podcastId param is set.
|
||||
*
|
||||
* @param integer|null $podcastId
|
||||
*/
|
||||
public function getBlockedDomains()
|
||||
{
|
||||
return $this->findAll();
|
||||
}
|
||||
|
||||
public function isDomainBlocked($domain)
|
||||
{
|
||||
if ($this->find($domain)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function blockDomain($name)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
// set all actors from the domain as blocked
|
||||
model('ActorModel')
|
||||
->where('domain', $name)
|
||||
->set('is_blocked', 1)
|
||||
->update();
|
||||
|
||||
$result = $this->insert([
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function unblockDomain($name)
|
||||
{
|
||||
$this->db->transStart();
|
||||
// unblock all actors from the domain
|
||||
model('ActorModel')
|
||||
->where('domain', $name)
|
||||
->set('is_blocked', 0)
|
||||
->update();
|
||||
|
||||
$result = $this->delete($name);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
178
app/Libraries/ActivityPub/Models/FavouriteModel.php
Normal file
178
app/Libraries/ActivityPub/Models/FavouriteModel.php
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
use ActivityPub\Activities\LikeActivity;
|
||||
use ActivityPub\Activities\UndoActivity;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
class FavouriteModel extends UuidModel
|
||||
{
|
||||
protected $table = 'activitypub_favourites';
|
||||
protected $uuidFields = ['note_id'];
|
||||
|
||||
protected $allowedFields = ['actor_id', 'note_id'];
|
||||
|
||||
protected $returnType = \ActivityPub\Entities\Favourite::class;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $updatedField = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param \ActivityPub\Entities\Actor $actor
|
||||
* @param \ActivityPub\Entities\Note $note
|
||||
* @param bool $registerActivity
|
||||
* @return void
|
||||
*/
|
||||
public function addFavourite($actor, $note, $registerActivity = true)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
$this->insert([
|
||||
'actor_id' => $actor->id,
|
||||
'note_id' => $note->id,
|
||||
]);
|
||||
|
||||
model('NoteModel')
|
||||
->where(
|
||||
'id',
|
||||
service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
)
|
||||
->increment('favourites_count');
|
||||
|
||||
Events::trigger('on_note_favourite', $actor, $note);
|
||||
|
||||
if ($registerActivity) {
|
||||
$likeActivity = new LikeActivity();
|
||||
$likeActivity->set('actor', $actor->uri)->set('object', $note->uri);
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Like',
|
||||
$actor->id,
|
||||
null,
|
||||
$note->id,
|
||||
$likeActivity->toJSON(),
|
||||
$note->published_at,
|
||||
'queued',
|
||||
);
|
||||
|
||||
$likeActivity->set(
|
||||
'id',
|
||||
url_to('activity', $actor->username, $activityId),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $likeActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
}
|
||||
|
||||
public function removeFavourite($actor, $note, $registerActivity = true)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
model('NoteModel')
|
||||
->where(
|
||||
'id',
|
||||
service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
)
|
||||
->decrement('favourites_count');
|
||||
|
||||
$this->table('activitypub_favourites')
|
||||
->where([
|
||||
'actor_id' => $actor->id,
|
||||
'note_id' => service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
])
|
||||
->delete();
|
||||
|
||||
Events::trigger('on_note_undo_favourite', $actor, $note);
|
||||
|
||||
if ($registerActivity) {
|
||||
$undoActivity = new UndoActivity();
|
||||
// get like activity
|
||||
$activity = model('ActivityModel')
|
||||
->where([
|
||||
'type' => 'Like',
|
||||
'actor_id' => $actor->id,
|
||||
'note_id' => service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
])
|
||||
->first();
|
||||
|
||||
$likeActivity = new LikeActivity();
|
||||
$likeActivity
|
||||
->set(
|
||||
'id',
|
||||
base_url(
|
||||
route_to('activity', $actor->username, $activity->id),
|
||||
),
|
||||
)
|
||||
->set('actor', $actor->uri)
|
||||
->set('object', $note->uri);
|
||||
|
||||
$undoActivity
|
||||
->set('actor', $actor->uri)
|
||||
->set('object', $likeActivity);
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Undo',
|
||||
$actor->id,
|
||||
null,
|
||||
$note->id,
|
||||
$undoActivity->toJSON(),
|
||||
$note->published_at,
|
||||
'queued',
|
||||
);
|
||||
|
||||
$undoActivity->set(
|
||||
'id',
|
||||
url_to('activity', $actor->username, $activityId),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $undoActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes favourite from database and increments count
|
||||
*
|
||||
* @param \ActivityPub\Entities\Actor $actor
|
||||
* @param \ActivityPub\Entities\Note $note
|
||||
* @return void
|
||||
*/
|
||||
public function toggleFavourite($actor, $note)
|
||||
{
|
||||
if (
|
||||
$this->where([
|
||||
'actor_id' => $actor->id,
|
||||
'note_id' => service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
])->first()
|
||||
) {
|
||||
$this->removeFavourite($actor, $note);
|
||||
} else {
|
||||
$this->addFavourite($actor, $note);
|
||||
}
|
||||
}
|
||||
}
|
||||
148
app/Libraries/ActivityPub/Models/FollowModel.php
Normal file
148
app/Libraries/ActivityPub/Models/FollowModel.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
use ActivityPub\Activities\FollowActivity;
|
||||
use ActivityPub\Activities\UndoActivity;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use CodeIgniter\Model;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class FollowModel extends Model
|
||||
{
|
||||
protected $table = 'activitypub_follows';
|
||||
|
||||
protected $allowedFields = ['actor_id', 'target_actor_id'];
|
||||
|
||||
protected $returnType = \ActivityPub\Entities\Follow::class;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $updatedField = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param \ActivityPub\Entities\Actor $actor
|
||||
* @param \ActivityPub\Entities\Actor $targetActor
|
||||
* @param bool $registerActivity
|
||||
* @return void
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function addFollower($actor, $targetActor, $registerActivity = true)
|
||||
{
|
||||
try {
|
||||
$this->db->transStart();
|
||||
|
||||
$this->insert([
|
||||
'actor_id' => $actor->id,
|
||||
'target_actor_id' => $targetActor->id,
|
||||
]);
|
||||
|
||||
// increment followers_count for target actor
|
||||
model('ActorModel')
|
||||
->where('id', $targetActor->id)
|
||||
->increment('followers_count');
|
||||
|
||||
if ($registerActivity) {
|
||||
$followActivity = new FollowActivity();
|
||||
|
||||
$followActivity
|
||||
->set('actor', $actor->uri)
|
||||
->set('object', $targetActor->uri);
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Follow',
|
||||
$actor->id,
|
||||
$targetActor->id,
|
||||
null,
|
||||
$followActivity->toJSON(),
|
||||
Time::now(),
|
||||
'queued',
|
||||
);
|
||||
|
||||
$followActivity->set(
|
||||
'id',
|
||||
base_url(
|
||||
route_to('activity', $actor->username, $activityId),
|
||||
),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $followActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
} catch (\Exception $e) {
|
||||
// follow already exists, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ActivityPub\Entities\Actor $actor
|
||||
* @param \ActivityPub\Entities\Actor $targetActor
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function removeFollower(
|
||||
$actor,
|
||||
$targetActor,
|
||||
$registerActivity = true
|
||||
) {
|
||||
$this->db->transStart();
|
||||
|
||||
$this->where([
|
||||
'actor_id' => $actor->id,
|
||||
'target_actor_id' => $targetActor->id,
|
||||
])->delete();
|
||||
|
||||
// decrement followers_count for target actor
|
||||
model('ActorModel')
|
||||
->where('id', $targetActor->id)
|
||||
->decrement('followers_count');
|
||||
|
||||
if ($registerActivity) {
|
||||
$undoActivity = new UndoActivity();
|
||||
// get follow activity from database
|
||||
$followActivity = model('ActivityModel')
|
||||
->where([
|
||||
'type' => 'Follow',
|
||||
'actor_id' => $actor->id,
|
||||
'target_actor_id' => $targetActor->id,
|
||||
])
|
||||
->first();
|
||||
|
||||
$undoActivity
|
||||
->set('actor', $actor->uri)
|
||||
->set('object', $followActivity->payload);
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Undo',
|
||||
$actor->id,
|
||||
$targetActor->id,
|
||||
null,
|
||||
$undoActivity->toJSON(),
|
||||
Time::now(),
|
||||
'queued',
|
||||
);
|
||||
|
||||
$undoActivity->set(
|
||||
'id',
|
||||
url_to('activity', $actor->username, $activityId),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $undoActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
}
|
||||
}
|
||||
548
app/Libraries/ActivityPub/Models/NoteModel.php
Normal file
548
app/Libraries/ActivityPub/Models/NoteModel.php
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
use ActivityPub\Entities\Note;
|
||||
use ActivityPub\Activities\AnnounceActivity;
|
||||
use ActivityPub\Activities\CreateActivity;
|
||||
use ActivityPub\Activities\DeleteActivity;
|
||||
use ActivityPub\Activities\UndoActivity;
|
||||
use ActivityPub\Objects\TombstoneObject;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class NoteModel extends UuidModel
|
||||
{
|
||||
protected $table = 'activitypub_notes';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $uuidFields = ['id', 'in_reply_to_id', 'reblog_of_id'];
|
||||
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'uri',
|
||||
'actor_id',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'message',
|
||||
'message_html',
|
||||
'favourites_count',
|
||||
'reblogs_count',
|
||||
'replies_count',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected $returnType = \ActivityPub\Entities\Note::class;
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $updatedField = null;
|
||||
|
||||
protected $validationRules = [
|
||||
'actor_id' => 'required',
|
||||
'message_html' => 'required_without[reblog_of_id]|max_length[500]',
|
||||
];
|
||||
|
||||
protected $beforeInsert = ['setNoteId'];
|
||||
|
||||
public function getNoteById($noteId)
|
||||
{
|
||||
return $this->find($noteId);
|
||||
}
|
||||
|
||||
public function getNoteByUri($noteUri)
|
||||
{
|
||||
return $this->where('uri', $noteUri)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all published notes for a given actor ordered by publication date
|
||||
*
|
||||
* @return \ActivityPub\Entities\Note[]
|
||||
*/
|
||||
public function getActorNotes($actorId)
|
||||
{
|
||||
return $this->where([
|
||||
'actor_id' => $actorId,
|
||||
'in_reply_to_id' => null,
|
||||
])
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->orderBy('published_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all published replies for a given note.
|
||||
* By default, it does not get replies from blocked actors.
|
||||
*
|
||||
* @param mixed $noteId
|
||||
* @param boolean $withBlocked false by default
|
||||
* @return array
|
||||
*/
|
||||
public function getNoteReplies($noteId, $withBlocked = false)
|
||||
{
|
||||
if (!$withBlocked) {
|
||||
$this->select('activitypub_notes.*')
|
||||
->join(
|
||||
'activitypub_actors',
|
||||
'activitypub_actors.id = activitypub_notes.actor_id',
|
||||
'inner',
|
||||
)
|
||||
->where('activitypub_actors.is_blocked', 0);
|
||||
}
|
||||
|
||||
$this->where(
|
||||
'in_reply_to_id',
|
||||
service('uuid')
|
||||
->fromString($noteId)
|
||||
->getBytes(),
|
||||
)
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->orderBy('published_at', 'ASC');
|
||||
|
||||
return $this->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all published reblogs for a given note
|
||||
*/
|
||||
public function getNoteReblogs($noteId)
|
||||
{
|
||||
return $this->where('reblog_of_id', $noteId)
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->orderBy('published_at', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function addPreviewCard($noteId, $previewCardId)
|
||||
{
|
||||
return $this->db->table('activitypub_notes_preview_cards')->insert([
|
||||
'note_id' => $noteId,
|
||||
'preview_card_id' => $previewCardId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds note in database along preview card if relevant
|
||||
*
|
||||
* @param \ActivityPub\Entities\Note $note
|
||||
* @param boolean $registerActivity
|
||||
* @param boolean $createPreviewCard
|
||||
* @return string|false returns the new note id if success or false otherwise
|
||||
*/
|
||||
public function addNote(
|
||||
$note,
|
||||
$createPreviewCard = true,
|
||||
$registerActivity = true
|
||||
) {
|
||||
helper('activitypub');
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
if (!($newNoteId = $this->insert($note, true))) {
|
||||
$this->db->transRollback();
|
||||
|
||||
// Couldn't insert note
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($createPreviewCard) {
|
||||
// parse message
|
||||
$messageUrls = extract_urls_from_message($note->message);
|
||||
|
||||
if (
|
||||
!empty($messageUrls) &&
|
||||
($previewCard = get_or_create_preview_card_from_url(
|
||||
new URI($messageUrls[0]),
|
||||
))
|
||||
) {
|
||||
if (!$this->addPreviewCard($newNoteId, $previewCard->id)) {
|
||||
$this->db->transRollback();
|
||||
|
||||
// problem when linking note to preview card
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $newNoteId;
|
||||
}
|
||||
}
|
||||
|
||||
model('ActorModel')
|
||||
->where('id', $note->actor_id)
|
||||
->increment('notes_count');
|
||||
|
||||
Events::trigger('on_note_add', $note);
|
||||
|
||||
if ($registerActivity) {
|
||||
$noteUuid = service('uuid')
|
||||
->fromBytes($newNoteId)
|
||||
->toString();
|
||||
|
||||
// set note id and uri to construct NoteObject
|
||||
$note->id = $noteUuid;
|
||||
$note->uri = base_url(
|
||||
route_to('note', $note->actor->username, $noteUuid),
|
||||
);
|
||||
|
||||
$createActivity = new CreateActivity();
|
||||
$noteObjectClass = config('ActivityPub')->noteObject;
|
||||
$createActivity
|
||||
->set('actor', $note->actor->uri)
|
||||
->set('object', new $noteObjectClass($note));
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Create',
|
||||
$note->actor_id,
|
||||
null,
|
||||
$noteUuid,
|
||||
$createActivity->toJSON(),
|
||||
$note->published_at,
|
||||
'queued',
|
||||
);
|
||||
|
||||
$createActivity->set(
|
||||
'id',
|
||||
base_url(
|
||||
route_to('activity', $note->actor->username, $activityId),
|
||||
),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $createActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $newNoteId;
|
||||
}
|
||||
|
||||
public function editNote($updatedNote)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
// update note create activity schedule in database
|
||||
$scheduledActivity = model('ActivityModel')
|
||||
->where([
|
||||
'type' => 'Create',
|
||||
'note_id' => service('uuid')
|
||||
->fromString($updatedNote->id)
|
||||
->getBytes(),
|
||||
])
|
||||
->first();
|
||||
|
||||
// update published date in payload
|
||||
$newPayload = $scheduledActivity->payload;
|
||||
$newPayload->object->published = $updatedNote->published_at->format(
|
||||
DATE_W3C,
|
||||
);
|
||||
model('ActivityModel')->update($scheduledActivity->id, [
|
||||
'payload' => json_encode($newPayload),
|
||||
'scheduled_at' => $updatedNote->published_at,
|
||||
]);
|
||||
|
||||
// update note
|
||||
$updateResult = $this->update($updatedNote->id, $updatedNote);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $updateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a note from the database and decrements meta data
|
||||
*
|
||||
* @param \ActivityPub\Entities\Note $note
|
||||
* @return mixed
|
||||
*/
|
||||
public function removeNote($note, $registerActivity = true)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
model('ActorModel')
|
||||
->where('id', $note->actor_id)
|
||||
->decrement('notes_count');
|
||||
|
||||
if ($note->in_reply_to_id) {
|
||||
// Note to remove is a reply
|
||||
model('NoteModel')
|
||||
->where(
|
||||
'id',
|
||||
service('uuid')
|
||||
->fromString($note->in_reply_to_id)
|
||||
->getBytes(),
|
||||
)
|
||||
->decrement('replies_count');
|
||||
}
|
||||
|
||||
// remove all reblogs
|
||||
foreach ($note->reblogs as $reblog) {
|
||||
$this->removeNote($reblog);
|
||||
}
|
||||
|
||||
// remove all replies
|
||||
foreach ($note->replies as $reply) {
|
||||
$this->removeNote($reply);
|
||||
}
|
||||
|
||||
Events::trigger('on_note_remove', $note);
|
||||
|
||||
if ($registerActivity) {
|
||||
$deleteActivity = new DeleteActivity();
|
||||
$tombstoneObject = new TombstoneObject();
|
||||
$tombstoneObject->set('id', $note->uri);
|
||||
$deleteActivity
|
||||
->set('actor', $note->actor->uri)
|
||||
->set('object', $tombstoneObject);
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Delete',
|
||||
$note->actor_id,
|
||||
null,
|
||||
null,
|
||||
$deleteActivity->toJSON(),
|
||||
Time::now(),
|
||||
'queued',
|
||||
);
|
||||
|
||||
$deleteActivity->set(
|
||||
'id',
|
||||
base_url(
|
||||
route_to('activity', $note->actor->username, $activityId),
|
||||
),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $deleteActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = model('NoteModel', false)->delete($note->id);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function addReply(
|
||||
$reply,
|
||||
$createPreviewCard = true,
|
||||
$registerActivity = true
|
||||
) {
|
||||
if (!$reply->in_reply_to_id) {
|
||||
throw new \Exception('Passed note is not a reply!');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
$noteId = $this->addNote($reply, $createPreviewCard, $registerActivity);
|
||||
|
||||
model('NoteModel')
|
||||
->where(
|
||||
'id',
|
||||
service('uuid')
|
||||
->fromString($reply->in_reply_to_id)
|
||||
->getBytes(),
|
||||
)
|
||||
->increment('replies_count');
|
||||
|
||||
Events::trigger('on_note_reply', $reply);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param \ActivityPub\Entities\Actor $actor
|
||||
* @param \ActivityPub\Entities\Note $note
|
||||
* @return ActivityPub\Models\BaseResult|int|string|false
|
||||
*/
|
||||
public function reblog($actor, $note, $registerActivity = true)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
$reblog = new Note([
|
||||
'actor_id' => $actor->id,
|
||||
'reblog_of_id' => $note->id,
|
||||
'published_at' => Time::now(),
|
||||
]);
|
||||
|
||||
// add reblog
|
||||
$reblogId = $this->insert($reblog, true);
|
||||
|
||||
model('ActorModel')
|
||||
->where('id', $actor->id)
|
||||
->increment('notes_count');
|
||||
|
||||
model('NoteModel')
|
||||
->where(
|
||||
'id',
|
||||
service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
)
|
||||
->increment('reblogs_count');
|
||||
|
||||
Events::trigger('on_note_reblog', $actor, $note);
|
||||
|
||||
if ($registerActivity) {
|
||||
$announceActivity = new AnnounceActivity($reblog);
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Announce',
|
||||
$actor->id,
|
||||
null,
|
||||
$note->id,
|
||||
$announceActivity->toJSON(),
|
||||
$reblog->published_at,
|
||||
'queued',
|
||||
);
|
||||
|
||||
$announceActivity->set(
|
||||
'id',
|
||||
base_url(
|
||||
route_to('activity', $note->actor->username, $activityId),
|
||||
),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $announceActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $reblogId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ActivityPub\Entities\Note $reblogNote
|
||||
* @return mixed
|
||||
*/
|
||||
public function undoReblog($reblogNote, $registerActivity = true)
|
||||
{
|
||||
$this->db->transStart();
|
||||
|
||||
model('ActorModel')
|
||||
->where('id', $reblogNote->actor_id)
|
||||
->decrement('notes_count');
|
||||
|
||||
model('NoteModel')
|
||||
->where(
|
||||
'id',
|
||||
service('uuid')
|
||||
->fromString($reblogNote->reblog_of_id)
|
||||
->getBytes(),
|
||||
)
|
||||
->decrement('reblogs_count');
|
||||
|
||||
Events::trigger('on_note_undo_reblog', $reblogNote);
|
||||
|
||||
if ($registerActivity) {
|
||||
$undoActivity = new UndoActivity();
|
||||
// get like activity
|
||||
$activity = model('ActivityModel')
|
||||
->where([
|
||||
'type' => 'Announce',
|
||||
'actor_id' => $reblogNote->actor_id,
|
||||
'note_id' => service('uuid')
|
||||
->fromString($reblogNote->reblog_of_id)
|
||||
->getBytes(),
|
||||
])
|
||||
->first();
|
||||
|
||||
$announceActivity = new AnnounceActivity($reblogNote);
|
||||
$announceActivity->set(
|
||||
'id',
|
||||
base_url(
|
||||
route_to(
|
||||
'activity',
|
||||
$reblogNote->actor->username,
|
||||
$activity->id,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$undoActivity
|
||||
->set('actor', $reblogNote->actor->uri)
|
||||
->set('object', $announceActivity);
|
||||
|
||||
$activityId = model('ActivityModel')->newActivity(
|
||||
'Undo',
|
||||
$reblogNote->actor_id,
|
||||
null,
|
||||
$reblogNote->reblog_of_id,
|
||||
$undoActivity->toJSON(),
|
||||
Time::now(),
|
||||
'queued',
|
||||
);
|
||||
|
||||
$undoActivity->set(
|
||||
'id',
|
||||
base_url(
|
||||
route_to(
|
||||
'activity',
|
||||
$reblogNote->actor->username,
|
||||
$activityId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
model('ActivityModel')->update($activityId, [
|
||||
'payload' => $undoActivity->toJSON(),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = model('NoteModel', false)->delete($reblogNote->id);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function toggleReblog($actor, $note)
|
||||
{
|
||||
if (
|
||||
!($reblogNote = $this->where([
|
||||
'actor_id' => $actor->id,
|
||||
'reblog_of_id' => service('uuid')
|
||||
->fromString($note->id)
|
||||
->getBytes(),
|
||||
])->first())
|
||||
) {
|
||||
$this->reblog($actor, $note);
|
||||
} else {
|
||||
$this->undoReblog($reblogNote);
|
||||
}
|
||||
}
|
||||
|
||||
protected function setNoteId($data)
|
||||
{
|
||||
$uuid4 = service('uuid')->uuid4();
|
||||
$data['id'] = $uuid4->toString();
|
||||
$data['data']['id'] = $uuid4->getBytes();
|
||||
|
||||
if (!isset($data['data']['uri'])) {
|
||||
$actor = model('ActorModel')->getActorById(
|
||||
$data['data']['actor_id'],
|
||||
);
|
||||
|
||||
$data['data']['uri'] = base_url(
|
||||
route_to('note', $actor->username, $uuid4->toString()),
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
56
app/Libraries/ActivityPub/Models/PreviewCardModel.php
Normal file
56
app/Libraries/ActivityPub/Models/PreviewCardModel.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PreviewCardModel extends Model
|
||||
{
|
||||
protected $table = 'activitypub_preview_cards';
|
||||
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'url',
|
||||
'title',
|
||||
'description',
|
||||
'type',
|
||||
'author_name',
|
||||
'author_url',
|
||||
'provider_name',
|
||||
'provider_url',
|
||||
'image',
|
||||
'html',
|
||||
];
|
||||
|
||||
protected $returnType = \ActivityPub\Entities\PreviewCard::class;
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
protected $useTimestamps = true;
|
||||
|
||||
public function getPreviewCardFromUrl($url)
|
||||
{
|
||||
return $this->where('url', $url)->first();
|
||||
}
|
||||
|
||||
public function getNotePreviewCard($noteId)
|
||||
{
|
||||
return $this->join(
|
||||
'activitypub_notes_preview_cards',
|
||||
'activitypub_notes_preview_cards.preview_card_id = id',
|
||||
'inner',
|
||||
)
|
||||
->where(
|
||||
'note_id',
|
||||
service('uuid')
|
||||
->fromString($noteId)
|
||||
->getBytes(),
|
||||
)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
206
app/Libraries/ActivityPub/Models/UuidModel.php
Normal file
206
app/Libraries/ActivityPub/Models/UuidModel.php
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Models;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DataException;
|
||||
use stdClass;
|
||||
|
||||
class UuidModel extends \Michalsn\Uuid\UuidModel
|
||||
{
|
||||
/**
|
||||
* This insert overwrite is added as a means to FIX some bugs
|
||||
* from the extended Uuid package. See: https://github.com/michalsn/codeigniter4-uuid/issues/2
|
||||
*
|
||||
* Inserts data into the current table. If an object is provided,
|
||||
* it will attempt to convert it to an array.
|
||||
*
|
||||
* @param array|object $data
|
||||
* @param boolean $returnID Whether insert ID should be returned or not.
|
||||
*
|
||||
* @return BaseResult|integer|string|false
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function insert($data = null, bool $returnID = true)
|
||||
{
|
||||
$escape = null;
|
||||
|
||||
$this->insertID = 0;
|
||||
|
||||
if (empty($data)) {
|
||||
$data = $this->tempData['data'] ?? null;
|
||||
$escape = $this->tempData['escape'] ?? null;
|
||||
$this->tempData = [];
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
throw DataException::forEmptyDataset('insert');
|
||||
}
|
||||
|
||||
// If $data is using a custom class with public or protected
|
||||
// properties representing the table elements, we need to grab
|
||||
// them as an array.
|
||||
if (is_object($data) && !$data instanceof stdClass) {
|
||||
$data = static::classToArray(
|
||||
$data,
|
||||
$this->primaryKey,
|
||||
$this->dateFormat,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// If it's still a stdClass, go ahead and convert to
|
||||
// an array so doProtectFields and other model methods
|
||||
// don't have to do special checks.
|
||||
if (is_object($data)) {
|
||||
$data = (array) $data;
|
||||
}
|
||||
|
||||
if (empty($data)) {
|
||||
throw DataException::forEmptyDataset('insert');
|
||||
}
|
||||
|
||||
// Validate data before saving.
|
||||
if ($this->skipValidation === false) {
|
||||
if ($this->cleanRules()->validate($data) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Must be called first so we don't
|
||||
// strip out created_at values.
|
||||
$data = $this->doProtectFields($data);
|
||||
|
||||
// Set created_at and updated_at with same time
|
||||
$date = $this->setDate();
|
||||
|
||||
if (
|
||||
$this->useTimestamps &&
|
||||
!empty($this->createdField) &&
|
||||
!array_key_exists($this->createdField, $data)
|
||||
) {
|
||||
$data[$this->createdField] = $date;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->useTimestamps &&
|
||||
!empty($this->updatedField) &&
|
||||
!array_key_exists($this->updatedField, $data)
|
||||
) {
|
||||
$data[$this->updatedField] = $date;
|
||||
}
|
||||
|
||||
$eventData = ['data' => $data];
|
||||
if ($this->tempAllowCallbacks) {
|
||||
$eventData = $this->trigger('beforeInsert', $eventData);
|
||||
}
|
||||
|
||||
// Require non empty primaryKey when
|
||||
// not using auto-increment feature
|
||||
if (
|
||||
!$this->useAutoIncrement &&
|
||||
empty($eventData['data'][$this->primaryKey])
|
||||
) {
|
||||
throw DataException::forEmptyPrimaryKey('insert');
|
||||
}
|
||||
|
||||
if (!empty($this->uuidFields)) {
|
||||
foreach ($this->uuidFields as $field) {
|
||||
if ($field === $this->primaryKey) {
|
||||
$this->uuidTempData[
|
||||
$field
|
||||
] = $this->uuid->{$this->uuidVersion}();
|
||||
|
||||
if ($this->uuidUseBytes === true) {
|
||||
$this->builder()->set(
|
||||
$field,
|
||||
$this->uuidTempData[$field]->getBytes(),
|
||||
);
|
||||
} else {
|
||||
$this->builder()->set(
|
||||
$field,
|
||||
$this->uuidTempData[$field]->toString(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
$this->uuidUseBytes === true &&
|
||||
!empty($eventData['data'][$field])
|
||||
) {
|
||||
$this->uuidTempData[$field] = $this->uuid->fromString(
|
||||
$eventData['data'][$field],
|
||||
);
|
||||
$this->builder()->set(
|
||||
$field,
|
||||
$this->uuidTempData[$field]->getBytes(),
|
||||
);
|
||||
unset($eventData['data'][$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Must use the set() method to ensure objects get converted to arrays
|
||||
$result = $this->builder()
|
||||
->set($eventData['data'], '', $escape)
|
||||
->insert();
|
||||
|
||||
// If insertion succeeded then save the insert ID
|
||||
if ($result) {
|
||||
if (
|
||||
!$this->useAutoIncrement ||
|
||||
isset($eventData['data'][$this->primaryKey])
|
||||
) {
|
||||
$this->insertID = $eventData['data'][$this->primaryKey];
|
||||
} else {
|
||||
if (in_array($this->primaryKey, $this->uuidFields)) {
|
||||
$this->insertID = $this->uuidTempData[
|
||||
$this->primaryKey
|
||||
]->toString();
|
||||
} else {
|
||||
$this->insertID = $this->db->insertID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup data before event trigger
|
||||
if (!empty($this->uuidFields) && $this->uuidUseBytes === true) {
|
||||
foreach ($this->uuidFields as $field) {
|
||||
if (
|
||||
$field === $this->primaryKey ||
|
||||
empty($this->uuidTempData[$field])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$eventData['data'][$field] = $this->uuidTempData[
|
||||
$field
|
||||
]->toString();
|
||||
}
|
||||
}
|
||||
|
||||
$eventData = [
|
||||
'id' => $this->insertID,
|
||||
'data' => $eventData['data'],
|
||||
'result' => $result,
|
||||
];
|
||||
if ($this->tempAllowCallbacks) {
|
||||
// Trigger afterInsert events with the inserted data and new ID
|
||||
$this->trigger('afterInsert', $eventData);
|
||||
}
|
||||
$this->tempAllowCallbacks = $this->allowCallbacks;
|
||||
|
||||
// If insertion failed, get out of here
|
||||
if (!$result) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// otherwise return the insertID, if requested.
|
||||
return $returnID ? $this->insertID : $result;
|
||||
}
|
||||
}
|
||||
113
app/Libraries/ActivityPub/Objects/ActorObject.php
Normal file
113
app/Libraries/ActivityPub/Objects/ActorObject.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Objects;
|
||||
|
||||
use ActivityPub\Core\ObjectType;
|
||||
|
||||
class ActorObject extends ObjectType
|
||||
{
|
||||
/**
|
||||
* @var array|string
|
||||
*/
|
||||
protected $context = [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Person';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $preferredUsername;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $summary;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $inbox;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $outbox;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $followers;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $icon;
|
||||
|
||||
/**
|
||||
* @var object
|
||||
*/
|
||||
protected $publicKey;
|
||||
|
||||
/**
|
||||
* @param \ActivityPub\Entities\Actor $podcast
|
||||
*/
|
||||
public function __construct($actor)
|
||||
{
|
||||
$this->id = $actor->uri;
|
||||
|
||||
$this->name = $actor->display_name;
|
||||
$this->preferredUsername = $actor->username;
|
||||
$this->summary = $actor->summary;
|
||||
$this->url = $actor->uri;
|
||||
|
||||
$this->inbox = $actor->inbox_url;
|
||||
$this->outbox = $actor->outbox_url;
|
||||
$this->followers = $actor->followers_url;
|
||||
|
||||
if ($actor->cover_image_url) {
|
||||
$this->image = [
|
||||
'type' => 'Image',
|
||||
'mediaType' => $actor->cover_image_mimetype,
|
||||
'url' => $actor->cover_image_url,
|
||||
];
|
||||
}
|
||||
$this->icon = [
|
||||
'type' => 'Image',
|
||||
'mediaType' => $actor->avatar_image_mimetype,
|
||||
'url' => $actor->avatar_image_url,
|
||||
];
|
||||
|
||||
$this->publicKey = [
|
||||
'id' => $actor->key_id,
|
||||
'owner' => $actor->uri,
|
||||
'publicKeyPem' => $actor->public_key,
|
||||
];
|
||||
}
|
||||
}
|
||||
61
app/Libraries/ActivityPub/Objects/NoteObject.php
Normal file
61
app/Libraries/ActivityPub/Objects/NoteObject.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This class defines the Object which is the
|
||||
* primary base type for the Activity Streams vocabulary.
|
||||
*
|
||||
* Object is a reserved word in php, so the class is named ObjectType.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Objects;
|
||||
|
||||
use ActivityPub\Core\ObjectType;
|
||||
|
||||
class NoteObject extends ObjectType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Note';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $attributedTo;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $inReplyTo;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $replies;
|
||||
|
||||
/**
|
||||
* @param \ActivityPub\Entities\Note $note
|
||||
*/
|
||||
public function __construct($note)
|
||||
{
|
||||
$this->id = $note->uri;
|
||||
|
||||
$this->content = $note->message_html;
|
||||
$this->published = $note->published_at->format(DATE_W3C);
|
||||
$this->attributedTo = $note->actor->uri;
|
||||
|
||||
if ($note->is_reply) {
|
||||
$this->inReplyTo = $note->reply_to_note->uri;
|
||||
}
|
||||
|
||||
$this->replies = base_url(
|
||||
route_to('note-replies', $note->actor->username, $note->id),
|
||||
);
|
||||
|
||||
$this->cc = [$note->actor->followers_url];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This class defines a Paginated OrderedCollection
|
||||
* based on CodeIgniter4 Pager to get the pagination metadata
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Objects;
|
||||
|
||||
use ActivityPub\Core\ObjectType;
|
||||
|
||||
class OrderedCollectionObject extends ObjectType
|
||||
{
|
||||
protected $type = 'OrderedCollection';
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
protected $totalItems;
|
||||
|
||||
/**
|
||||
* @var integer|null
|
||||
*/
|
||||
protected $first;
|
||||
|
||||
/**
|
||||
* @var integer|null
|
||||
*/
|
||||
protected $current;
|
||||
|
||||
/**
|
||||
* @var integer|null
|
||||
*/
|
||||
protected $last;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected $orderedItems;
|
||||
|
||||
/**
|
||||
* @param \ActivityPub\Libraries\ActivityPub\Activity[] $orderedItems
|
||||
* @param \CodeIgniter\Pager\Pager $pager
|
||||
*/
|
||||
public function __construct($orderedItems, $pager = null)
|
||||
{
|
||||
$this->id = current_url();
|
||||
|
||||
if ($pager) {
|
||||
$totalItems = $pager->getTotal();
|
||||
$this->totalItems = $totalItems;
|
||||
|
||||
if ($totalItems) {
|
||||
$this->first = $pager->getPageURI($pager->getFirstPage());
|
||||
$this->current = $pager->getPageURI();
|
||||
$this->last = $pager->getPageURI($pager->getLastPage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->orderedItems = $orderedItems;
|
||||
}
|
||||
}
|
||||
55
app/Libraries/ActivityPub/Objects/OrderedCollectionPage.php
Normal file
55
app/Libraries/ActivityPub/Objects/OrderedCollectionPage.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This class defines a Paginated OrderedCollection
|
||||
* based on CodeIgniter4 Pager to get the pagination metadata
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Objects;
|
||||
|
||||
class OrderedCollectionPage extends OrderedCollectionObject
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'OrderedCollectionPage';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $partOf;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
protected $prev;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
protected $next;
|
||||
|
||||
/**
|
||||
* @param \CodeIgniter\Pager\Pager $pager
|
||||
* @param \ActivityPub\Libraries\ActivityPub\Activity[] $orderedItems
|
||||
*/
|
||||
public function __construct($pager, $orderedItems)
|
||||
{
|
||||
parent::__construct($orderedItems, $pager);
|
||||
|
||||
$isFirstPage = $pager->getCurrentPage() === $pager->getFirstPage();
|
||||
$isLastPage = $pager->getCurrentPage() === $pager->getLastPage();
|
||||
$isFirstPage && ($this->first = null);
|
||||
$isLastPage && ($this->last = null);
|
||||
|
||||
$this->id = $pager->getPageURI($pager->getCurrentPage());
|
||||
$this->partOf = $pager->getPageURI();
|
||||
$this->prev = $pager->getPreviousPageURI();
|
||||
$this->current = $pager->getPageURI($pager->getCurrentPage());
|
||||
$this->next = $pager->getNextPageURI();
|
||||
}
|
||||
}
|
||||
19
app/Libraries/ActivityPub/Objects/TombstoneObject.php
Normal file
19
app/Libraries/ActivityPub/Objects/TombstoneObject.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub\Objects;
|
||||
|
||||
use ActivityPub\Core\ObjectType;
|
||||
|
||||
class TombstoneObject extends ObjectType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Tombstone';
|
||||
}
|
||||
126
app/Libraries/ActivityPub/WebFinger.php
Normal file
126
app/Libraries/ActivityPub/WebFinger.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace ActivityPub;
|
||||
|
||||
use Exception;
|
||||
|
||||
class WebFinger
|
||||
{
|
||||
const RESOURCE_PATTERN = '/^acct:(?P<username>([\w_]+))@(?P<domain>([\w\-\.]+[\w]+)(:[\d]+)?)$/x';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $username;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $port;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $subject;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $aliases;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $links;
|
||||
|
||||
/**
|
||||
* @param string $resource
|
||||
*/
|
||||
public function __construct($resource)
|
||||
{
|
||||
$this->subject = $resource;
|
||||
|
||||
// Split resource into its parts (username, domain)
|
||||
$parts = $this->splitResource($resource);
|
||||
if (!$parts) {
|
||||
throw new Exception('Wrong WebFinger resource pattern.');
|
||||
}
|
||||
extract($parts);
|
||||
|
||||
$this->username = $username;
|
||||
$this->domain = $domain;
|
||||
|
||||
$currentUrl = current_url(true);
|
||||
$currentDomain =
|
||||
$currentUrl->getHost() .
|
||||
($currentUrl->getPort() ? ':' . $currentUrl->getPort() : '');
|
||||
if ($currentDomain !== $domain) {
|
||||
// TODO: return error code
|
||||
throw new Exception('Domain does not correspond to Instance.');
|
||||
}
|
||||
|
||||
if (
|
||||
!($actor = model('ActorModel')->getActorByUsername(
|
||||
$username,
|
||||
$domain,
|
||||
))
|
||||
) {
|
||||
throw new Exception('Could not find actor');
|
||||
}
|
||||
|
||||
$this->aliases = [$actor->id];
|
||||
$this->links = [
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $actor->uri,
|
||||
],
|
||||
[
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => $actor->uri, # TODO: should there be 2 values? @actorUsername
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Split resource into its parts (username, domain)
|
||||
*
|
||||
* @param string $resource
|
||||
* @return bool|array
|
||||
*/
|
||||
private function splitResource(string $resource)
|
||||
{
|
||||
if (!preg_match(self::RESOURCE_PATTERN, $resource, $matches)) {
|
||||
// Resource pattern failed
|
||||
return false;
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebFinger response as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'subject' => $this->subject,
|
||||
'aliases' => $this->aliases,
|
||||
'links' => $this->links,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Generates and renders a breadcrumb based on the current url segments
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
|
|
|||
153
app/Libraries/Image.php
Normal file
153
app/Libraries/Image.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
class Image
|
||||
{
|
||||
/**
|
||||
* @var \Config\Images
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $original_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $original_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $thumbnail_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $thumbnail_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $medium_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $medium_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $large_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $large_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $feed_path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $feed_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $id3_path;
|
||||
|
||||
public function __construct($originalUri, $mimetype)
|
||||
{
|
||||
helper('media');
|
||||
|
||||
$originalPath = media_path($originalUri);
|
||||
|
||||
[
|
||||
'filename' => $filename,
|
||||
'dirname' => $dirname,
|
||||
'extension' => $extension,
|
||||
] = pathinfo($originalPath);
|
||||
|
||||
// load images extensions from config
|
||||
$this->config = config('Images');
|
||||
|
||||
$thumbnailExtension = $this->config->thumbnailExtension;
|
||||
$mediumExtension = $this->config->mediumExtension;
|
||||
$largeExtension = $this->config->largeExtension;
|
||||
$feedExtension = $this->config->feedExtension;
|
||||
$id3Extension = $this->config->id3Extension;
|
||||
|
||||
$thumbnail =
|
||||
$dirname . '/' . $filename . $thumbnailExtension . '.' . $extension;
|
||||
$medium =
|
||||
$dirname . '/' . $filename . $mediumExtension . '.' . $extension;
|
||||
$large =
|
||||
$dirname . '/' . $filename . $largeExtension . '.' . $extension;
|
||||
$feed = $dirname . '/' . $filename . $feedExtension . '.' . $extension;
|
||||
$id3 = $dirname . '/' . $filename . $id3Extension . '.' . $extension;
|
||||
|
||||
$this->original_path = $originalPath;
|
||||
$this->original_url = media_url($originalUri);
|
||||
$this->thumbnail_path = $thumbnail;
|
||||
$this->thumbnail_url = base_url($thumbnail);
|
||||
$this->medium_path = $medium;
|
||||
$this->medium_url = base_url($medium);
|
||||
$this->large_path = $large;
|
||||
$this->large_url = base_url($large);
|
||||
$this->feed_path = $feed;
|
||||
$this->feed_url = base_url($feed);
|
||||
$this->id3_path = $id3;
|
||||
|
||||
$this->mimetype = $mimetype;
|
||||
}
|
||||
|
||||
public function saveSizes()
|
||||
{
|
||||
$thumbnailSize = $this->config->thumbnailSize;
|
||||
$mediumSize = $this->config->mediumSize;
|
||||
$largeSize = $this->config->largeSize;
|
||||
$feedSize = $this->config->feedSize;
|
||||
$id3Size = $this->config->id3Size;
|
||||
|
||||
$imageService = \Config\Services::image();
|
||||
|
||||
$imageService
|
||||
->withFile($this->original_path)
|
||||
->resize($thumbnailSize, $thumbnailSize)
|
||||
->save($this->thumbnail_path);
|
||||
|
||||
$imageService
|
||||
->withFile($this->original_path)
|
||||
->resize($mediumSize, $mediumSize)
|
||||
->save($this->medium_path);
|
||||
|
||||
$imageService
|
||||
->withFile($this->original_path)
|
||||
->resize($largeSize, $largeSize)
|
||||
->save($this->large_path);
|
||||
|
||||
$imageService
|
||||
->withFile($this->original_path)
|
||||
->resize($feedSize, $feedSize)
|
||||
->save($this->feed_path);
|
||||
|
||||
$imageService
|
||||
->withFile($this->original_path)
|
||||
->resize($id3Size, $id3Size)
|
||||
->save($this->id3_path);
|
||||
}
|
||||
}
|
||||
14
app/Libraries/Negotiate.php
Normal file
14
app/Libraries/Negotiate.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
class Negotiate extends \CodeIgniter\HTTP\Negotiate
|
||||
{
|
||||
public function callMatch(
|
||||
array $acceptable,
|
||||
string $supported,
|
||||
bool $enforceTypes = false
|
||||
): bool {
|
||||
return $this->match($acceptable, $supported, $enforceTypes);
|
||||
}
|
||||
}
|
||||
30
app/Libraries/NoteObject.php
Normal file
30
app/Libraries/NoteObject.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
class NoteObject extends \ActivityPub\Objects\NoteObject
|
||||
{
|
||||
/**
|
||||
* @param \App\Entities\Note $note
|
||||
*/
|
||||
public function __construct($note)
|
||||
{
|
||||
parent::__construct($note);
|
||||
|
||||
if ($note->episode_id) {
|
||||
$this->content =
|
||||
'<a href="' .
|
||||
$note->episode->link .
|
||||
'" target="_blank" rel="noopener noreferrer">' .
|
||||
$note->episode->title .
|
||||
'</a><br/>' .
|
||||
$note->message_html;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
app/Libraries/PodcastActor.php
Normal file
31
app/Libraries/PodcastActor.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use App\Models\PodcastModel;
|
||||
|
||||
class PodcastActor extends \ActivityPub\Objects\ActorObject
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rss;
|
||||
|
||||
/**
|
||||
* @param \App\Entities\Actor $actor
|
||||
*/
|
||||
public function __construct($actor)
|
||||
{
|
||||
parent::__construct($actor);
|
||||
|
||||
$podcast = (new PodcastModel())->where('actor_id', $actor->id)->first();
|
||||
|
||||
$this->rss = $podcast->feed_url;
|
||||
}
|
||||
}
|
||||
205
app/Libraries/Router.php
Normal file
205
app/Libraries/Router.php
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file extends the Router class from the CodeIgniter 4 framework.
|
||||
*
|
||||
* It introduces the alternate-content option for a route.
|
||||
*
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use Config\Services;
|
||||
use CodeIgniter\Router\Exceptions\RedirectException;
|
||||
|
||||
class Router extends \CodeIgniter\Router\Router
|
||||
{
|
||||
/**
|
||||
* Compares the uri string against the routes that the
|
||||
* RouteCollection class defined for us, attempting to find a match.
|
||||
* This method will modify $this->controller, etal as needed.
|
||||
*
|
||||
* @param string $uri The URI path to compare against the routes
|
||||
*
|
||||
* @return boolean Whether the route was matched or not.
|
||||
* @throws RedirectException
|
||||
*/
|
||||
protected function checkRoutes(string $uri): bool
|
||||
{
|
||||
$routes = $this->collection->getRoutes(
|
||||
$this->collection->getHTTPVerb(),
|
||||
);
|
||||
|
||||
// Don't waste any time
|
||||
if (empty($routes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uri = $uri === '/' ? $uri : ltrim($uri, '/ ');
|
||||
|
||||
// Loop through the route array looking for wildcards
|
||||
foreach ($routes as $key => $val) {
|
||||
// Reset localeSegment
|
||||
$localeSegment = null;
|
||||
|
||||
$key = $key === '/' ? $key : ltrim($key, '/ ');
|
||||
|
||||
$matchedKey = $key;
|
||||
|
||||
// Are we dealing with a locale?
|
||||
if (strpos($key, '{locale}') !== false) {
|
||||
$localeSegment = array_search(
|
||||
'{locale}',
|
||||
preg_split(
|
||||
'/[\/]*((^[a-zA-Z0-9])|\(([^()]*)\))*[\/]+/m',
|
||||
$key,
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
// Replace it with a regex so it
|
||||
// will actually match.
|
||||
$key = str_replace('/', '\/', $key);
|
||||
$key = str_replace('{locale}', '[^\/]+', $key);
|
||||
}
|
||||
|
||||
// Does the RegEx match?
|
||||
if (preg_match('#^' . $key . '$#u', $uri, $matches)) {
|
||||
$this->matchedRouteOptions = $this->collection->getRoutesOptions(
|
||||
$matchedKey,
|
||||
);
|
||||
|
||||
// Is this route supposed to redirect to another?
|
||||
if ($this->collection->isRedirect($key)) {
|
||||
throw new RedirectException(
|
||||
is_array($val) ? key($val) : $val,
|
||||
$this->collection->getRedirectCode($key),
|
||||
);
|
||||
}
|
||||
// Store our locale so CodeIgniter object can
|
||||
// assign it to the Request.
|
||||
if (isset($localeSegment)) {
|
||||
// The following may be inefficient, but doesn't upset NetBeans :-/
|
||||
$temp = explode('/', $uri);
|
||||
$this->detectedLocale = $temp[$localeSegment];
|
||||
}
|
||||
|
||||
// Are we using Closures? If so, then we need
|
||||
// to collect the params into an array
|
||||
// so it can be passed to the controller method later.
|
||||
if (!is_string($val) && is_callable($val)) {
|
||||
$this->controller = $val;
|
||||
|
||||
// Remove the original string from the matches array
|
||||
array_shift($matches);
|
||||
|
||||
$this->params = $matches;
|
||||
|
||||
$this->matchedRoute = [$matchedKey, $val];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is there an alternate content for the matchedRoute?
|
||||
|
||||
// check if the alternate-content has been requested in the accept
|
||||
// header and overwrite the $val with the matching controller method
|
||||
if (
|
||||
array_key_exists(
|
||||
'alternate-content',
|
||||
$this->matchedRouteOptions,
|
||||
) &&
|
||||
is_array($this->matchedRouteOptions['alternate-content'])
|
||||
) {
|
||||
$request = Services::request();
|
||||
$negotiate = Services::negotiator();
|
||||
|
||||
$acceptHeader = $request->getHeader('Accept')->getValue();
|
||||
$parsedHeader = $negotiate->parseHeader($acceptHeader);
|
||||
|
||||
$supported = array_keys(
|
||||
$this->matchedRouteOptions['alternate-content'],
|
||||
);
|
||||
|
||||
$expectedContentType = $parsedHeader[0];
|
||||
foreach ($supported as $available) {
|
||||
if (
|
||||
$negotiate->callMatch(
|
||||
$expectedContentType,
|
||||
$available,
|
||||
true,
|
||||
)
|
||||
) {
|
||||
if (
|
||||
array_key_exists(
|
||||
'namespace',
|
||||
$this->matchedRouteOptions[
|
||||
'alternate-content'
|
||||
][$available],
|
||||
)
|
||||
) {
|
||||
$this->collection->setDefaultNamespace(
|
||||
$this->matchedRouteOptions[
|
||||
'alternate-content'
|
||||
][$available]['namespace'],
|
||||
);
|
||||
}
|
||||
$val =
|
||||
$this->collection->getDefaultNamespace() .
|
||||
$this->directory .
|
||||
$this->matchedRouteOptions['alternate-content'][
|
||||
$available
|
||||
]['controller-method'];
|
||||
|
||||
// no need to continue loop as $val has been overwritten
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Are we using the default method for back-references?
|
||||
|
||||
// Support resource route when function with subdirectory
|
||||
// ex: $routes->resource('Admin/Admins');
|
||||
if (
|
||||
strpos($val, '$') !== false &&
|
||||
strpos($key, '(') !== false &&
|
||||
strpos($key, '/') !== false
|
||||
) {
|
||||
$replacekey = str_replace('/(.*)', '', $key);
|
||||
$val = preg_replace('#^' . $key . '$#u', $val, $uri);
|
||||
$val = str_replace(
|
||||
$replacekey,
|
||||
str_replace('/', '\\', $replacekey),
|
||||
$val,
|
||||
);
|
||||
} elseif (
|
||||
strpos($val, '$') !== false &&
|
||||
strpos($key, '(') !== false
|
||||
) {
|
||||
$val = preg_replace('#^' . $key . '$#u', $val, $uri);
|
||||
} elseif (strpos($val, '/') !== false) {
|
||||
[$controller, $method] = explode('::', $val);
|
||||
|
||||
// Only replace slashes in the controller, not in the method.
|
||||
$controller = str_replace('/', '\\', $controller);
|
||||
|
||||
$val = $controller . '::' . $method;
|
||||
}
|
||||
|
||||
$this->setRequest(explode('/', $val));
|
||||
|
||||
$this->matchedRoute = [$matchedKey, $val];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -34,24 +34,24 @@ class AnalyticsPodcastByCountryModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_by_country_weekly"
|
||||
"{$podcastId}_analytics_podcast_by_country_weekly",
|
||||
))
|
||||
) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select('`country_code` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('country_code as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_by_country_weekly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -68,24 +68,24 @@ class AnalyticsPodcastByCountryModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_by_country_yearly"
|
||||
"{$podcastId}_analytics_podcast_by_country_yearly",
|
||||
))
|
||||
) {
|
||||
$oneYearAgo = date('Y-m-d', strtotime('-1 year'));
|
||||
$found = $this->select('`country_code` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('country_code as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => $oneYearAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => $oneYearAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_by_country_yearly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -33,25 +33,25 @@ class AnalyticsPodcastByEpisodeModel extends Model
|
|||
if (!$episodeId) {
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_by_episode_by_day"
|
||||
"{$podcastId}_analytics_podcast_by_episode_by_day",
|
||||
))
|
||||
) {
|
||||
$lastEpisodes = (new EpisodeModel())
|
||||
->select('`id`, `season_number`, `number`, `title`')
|
||||
->orderBy('`id`', 'DESC')
|
||||
->where(['`podcast_id`' => $podcastId])
|
||||
->select('id, season_number, number, title')
|
||||
->orderBy('id', 'DESC')
|
||||
->where(['podcast_id' => $podcastId])
|
||||
->findAll(5);
|
||||
|
||||
$found = $this->select('`age` AS `X`');
|
||||
$found = $this->select('age AS X');
|
||||
|
||||
$letter = 97;
|
||||
foreach ($lastEpisodes as $episode) {
|
||||
$found = $found
|
||||
->selectSum(
|
||||
'(CASE WHEN `episode_id`=' .
|
||||
'(CASE WHEN episode_id=' .
|
||||
$episode->id .
|
||||
' THEN `hits` END)',
|
||||
'`' . chr($letter) . 'Y`'
|
||||
' THEN hits END)',
|
||||
'' . chr($letter) . 'Y',
|
||||
)
|
||||
->select(
|
||||
'"' .
|
||||
|
|
@ -62,50 +62,50 @@ class AnalyticsPodcastByEpisodeModel extends Model
|
|||
? ''
|
||||
: '-' . $episode->number . '/ ') .
|
||||
$episode->title .
|
||||
'" AS `' .
|
||||
'" AS ' .
|
||||
chr($letter) .
|
||||
'Value`'
|
||||
'Value',
|
||||
);
|
||||
$letter++;
|
||||
}
|
||||
|
||||
$found = $found
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`age` <' => 60,
|
||||
'podcast_id' => $podcastId,
|
||||
'age <' => 60,
|
||||
])
|
||||
->groupBy('`X`')
|
||||
->orderBy('`X`', 'ASC')
|
||||
->groupBy('X')
|
||||
->orderBy('X', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_by_episode_by_day",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
} else {
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day"
|
||||
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
|
||||
))
|
||||
) {
|
||||
$found = $this->select('`date as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('date as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`episode_id`' => $episodeId,
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`age` <' => 60,
|
||||
'episode_id' => $episodeId,
|
||||
'podcast_id' => $podcastId,
|
||||
'age <' => 60,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -121,23 +121,23 @@ class AnalyticsPodcastByEpisodeModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month"
|
||||
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month",
|
||||
))
|
||||
) {
|
||||
$found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('DATE_FORMAT(date,"%Y-%m-01") as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'episode_id' => $episodeId,
|
||||
'podcast_id' => $podcastId,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -34,21 +34,21 @@ class AnalyticsPodcastByHourModel extends Model
|
|||
{
|
||||
if (!($found = cache("{$podcastId}_analytics_podcasts_by_hour"))) {
|
||||
$found = $this->select(
|
||||
'right(concat(\'0\',`hour`,\'h\'),3) as `labels`'
|
||||
'right(concat(\'0\',hour,\'h\'),3) as labels',
|
||||
)
|
||||
->selectSum('`hits`', '`values`')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => date('Y-m-d', strtotime('-60 days')),
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => date('Y-m-d', strtotime('-60 days')),
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_hour",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -34,25 +34,25 @@ class AnalyticsPodcastByPlayerModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_app_weekly"
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_app_weekly",
|
||||
))
|
||||
) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select('`app` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('app as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`app` !=' => '',
|
||||
'`is_bot`' => 0,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'app !=' => '',
|
||||
'is_bot' => 0,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_app_weekly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -69,25 +69,25 @@ class AnalyticsPodcastByPlayerModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_app_yearly"
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_app_yearly",
|
||||
))
|
||||
) {
|
||||
$oneYearAgo = date('Y-m-d', strtotime('-1 year'));
|
||||
$found = $this->select('`app` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('app as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`app` !=' => '',
|
||||
'`is_bot`' => 0,
|
||||
'`date` >' => $oneYearAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'app !=' => '',
|
||||
'is_bot' => 0,
|
||||
'date >' => $oneYearAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_app_yearly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -104,26 +104,26 @@ class AnalyticsPodcastByPlayerModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_os_weekly"
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_os_weekly",
|
||||
))
|
||||
) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select('`os` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('os as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`app` !=' => '',
|
||||
'`os` !=' => '',
|
||||
'`is_bot`' => 0,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'app !=' => '',
|
||||
'os !=' => '',
|
||||
'is_bot' => 0,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_os_weekly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -140,25 +140,25 @@ class AnalyticsPodcastByPlayerModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_device_weekly"
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_device_weekly",
|
||||
))
|
||||
) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select('`device` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('device as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`device` !=' => '',
|
||||
'`is_bot`' => 0,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'device !=' => '',
|
||||
'is_bot' => 0,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_player_by_device_weekly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -177,21 +177,21 @@ class AnalyticsPodcastByPlayerModel extends Model
|
|||
!($found = cache("{$podcastId}_analytics_podcasts_by_player_bots"))
|
||||
) {
|
||||
$oneYearAgo = date('Y-m-d', strtotime('-1 year'));
|
||||
$found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('DATE_FORMAT(date,"%Y-%m-01") as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`is_bot`' => 1,
|
||||
'`date` >' => $oneYearAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'is_bot' => 1,
|
||||
'date >' => $oneYearAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_player_bots",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -35,25 +35,25 @@ class AnalyticsPodcastByRegionModel extends Model
|
|||
$locale = service('request')->getLocale();
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_by_region_{$locale}"
|
||||
"{$podcastId}_analytics_podcast_by_region_{$locale}",
|
||||
))
|
||||
) {
|
||||
$found = $this->select('`country_code`, `region_code`')
|
||||
->selectSum('`hits`', '`value`')
|
||||
->selectAvg('`latitude`')
|
||||
->selectAvg('`longitude`')
|
||||
->groupBy('`country_code`, `region_code`')
|
||||
$found = $this->select('country_code, region_code')
|
||||
->selectSum('hits', 'value')
|
||||
->selectAvg('latitude')
|
||||
->selectAvg('longitude')
|
||||
->groupBy('country_code, region_code')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => date('Y-m-d', strtotime('-1 week')),
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => date('Y-m-d', strtotime('-1 week')),
|
||||
])
|
||||
->orderBy('`value`', 'DESC')
|
||||
->orderBy('value', 'DESC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_by_region_{$locale}",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -34,25 +34,25 @@ class AnalyticsPodcastByServiceModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcasts_by_service_weekly"
|
||||
"{$podcastId}_analytics_podcasts_by_service_weekly",
|
||||
))
|
||||
) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select('`service` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('service as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`service` !=' => '',
|
||||
'`is_bot`' => 0,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'service !=' => '',
|
||||
'is_bot' => 0,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_service_weekly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ class AnalyticsPodcastModel extends Model
|
|||
public function getDataByDay(int $podcastId): array
|
||||
{
|
||||
if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) {
|
||||
$found = $this->select('`date` as `labels`, `hits` as `values`')
|
||||
$found = $this->select('date as labels, hits as values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => date('Y-m-d', strtotime('-60 days')),
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => date('Y-m-d', strtotime('-60 days')),
|
||||
])
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save("{$podcastId}_analytics_podcast_by_day", $found, 600);
|
||||
|
|
@ -57,21 +57,21 @@ class AnalyticsPodcastModel extends Model
|
|||
{
|
||||
if (!($found = cache("{$podcastId}_analytics_podcasts_by_weekday"))) {
|
||||
$found = $this->select(
|
||||
'LEFT(DAYNAME(`date`),3) as `labels`, WEEKDAY(`date`) as `sort_labels`'
|
||||
'LEFT(DAYNAME(date),3) as labels, WEEKDAY(date) as sort_labels',
|
||||
)
|
||||
->selectSum('`hits`', '`values`')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => date('Y-m-d', strtotime('-60 days')),
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => date('Y-m-d', strtotime('-60 days')),
|
||||
])
|
||||
->groupBy('`labels`, `sort_labels`')
|
||||
->orderBy('`sort_labels`', 'ASC')
|
||||
->groupBy('labels, sort_labels')
|
||||
->orderBy('sort_labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcasts_by_weekday",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -88,19 +88,19 @@ class AnalyticsPodcastModel extends Model
|
|||
{
|
||||
if (!($found = cache("{$podcastId}_analytics_podcast_by_bandwidth"))) {
|
||||
$found = $this->select(
|
||||
'`date` as `labels`, round(`bandwidth` / 1048576, 1) as `values`'
|
||||
'date as labels, round(bandwidth / 1048576, 1) as `values`',
|
||||
)
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => date('Y-m-d', strtotime('-60 days')),
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => date('Y-m-d', strtotime('-60 days')),
|
||||
])
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_by_bandwidth",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -116,19 +116,19 @@ class AnalyticsPodcastModel extends Model
|
|||
public function getDataByMonth(int $podcastId): array
|
||||
{
|
||||
if (!($found = cache("{$podcastId}_analytics_podcast_by_month"))) {
|
||||
$found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('DATE_FORMAT(date,"%Y-%m-01") as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'podcast_id' => $podcastId,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_by_month",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -145,23 +145,21 @@ class AnalyticsPodcastModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_unique_listeners_by_day"
|
||||
"{$podcastId}_analytics_podcast_unique_listeners_by_day",
|
||||
))
|
||||
) {
|
||||
$found = $this->select(
|
||||
'`date` as `labels`, `unique_listeners` as `values`'
|
||||
)
|
||||
$found = $this->select('date as labels, unique_listeners as values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => date('Y-m-d', strtotime('-60 days')),
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => date('Y-m-d', strtotime('-60 days')),
|
||||
])
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_unique_listeners_by_day",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -178,22 +176,22 @@ class AnalyticsPodcastModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_unique_listeners_by_month"
|
||||
"{$podcastId}_analytics_podcast_unique_listeners_by_month",
|
||||
))
|
||||
) {
|
||||
$found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`')
|
||||
->selectSum('`unique_listeners`', '`values`')
|
||||
$found = $this->select('DATE_FORMAT(date,"%Y-%m-01") as labels')
|
||||
->selectSum('unique_listeners', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'podcast_id' => $podcastId,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_unique_listeners_by_month",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -210,7 +208,7 @@ class AnalyticsPodcastModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_listening_time_by_day"
|
||||
"{$podcastId}_analytics_podcast_listening_time_by_day",
|
||||
))
|
||||
) {
|
||||
$found = $this->select('date as labels')
|
||||
|
|
@ -226,7 +224,7 @@ class AnalyticsPodcastModel extends Model
|
|||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_listening_time_by_day",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -243,22 +241,22 @@ class AnalyticsPodcastModel extends Model
|
|||
{
|
||||
if (
|
||||
!($found = cache(
|
||||
"{$podcastId}_analytics_podcast_listening_time_by_month"
|
||||
"{$podcastId}_analytics_podcast_listening_time_by_month",
|
||||
))
|
||||
) {
|
||||
$found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`')
|
||||
$found = $this->select('DATE_FORMAT(date,"%Y-%m-01") as labels')
|
||||
->selectSum('duration', 'values')
|
||||
->where([
|
||||
$this->table . '.podcast_id' => $podcastId,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`labels`', 'ASC')
|
||||
->groupBy('labels')
|
||||
->orderBy('labels', 'ASC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_podcast_listening_time_by_month",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -34,19 +34,20 @@ class AnalyticsWebsiteByBrowserModel extends Model
|
|||
{
|
||||
if (!($found = cache("{$podcastId}_analytics_website_by_browser"))) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select('`browser` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('browser as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_website_by_browser",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -35,20 +35,20 @@ class AnalyticsWebsiteByEntryPageModel extends Model
|
|||
if (!($found = cache("{$podcastId}_analytics_website_by_entry_page"))) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select(
|
||||
'IF(`entry_page_url`=\'/\',\'/\',SUBSTRING_INDEX(`entry_page_url`,\'/\',-1)) as `labels`'
|
||||
'IF(entry_page_url=\'/\',\'/\',SUBSTRING_INDEX(entry_page_url,\'/\',-1)) as labels',
|
||||
)
|
||||
->selectSum('`hits`', '`values`')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_website_by_entry_page",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
|
|
@ -34,19 +34,19 @@ class AnalyticsWebsiteByRefererModel extends Model
|
|||
{
|
||||
if (!($found = cache("{$podcastId}_analytics_website_by_referer"))) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select('`referer_url` as `labels`')
|
||||
->selectSum('`hits`', '`values`')
|
||||
$found = $this->select('referer_url as labels')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_website_by_referer",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -66,20 +66,20 @@ class AnalyticsWebsiteByRefererModel extends Model
|
|||
) {
|
||||
$oneWeekAgo = date('Y-m-d', strtotime('-1 week'));
|
||||
$found = $this->select(
|
||||
'SUBSTRING_INDEX(`domain`, \'.\', -2) as `labels`'
|
||||
'SUBSTRING_INDEX(domain, \'.\', -2) as labels',
|
||||
)
|
||||
->selectSum('`hits`', '`values`')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => $oneWeekAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => $oneWeekAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_website_by_domain_weekly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
@ -99,20 +99,20 @@ class AnalyticsWebsiteByRefererModel extends Model
|
|||
) {
|
||||
$oneYearAgo = date('Y-m-d', strtotime('-1 year'));
|
||||
$found = $this->select(
|
||||
'SUBSTRING_INDEX(`domain`, \'.\', -2) as `labels`'
|
||||
'SUBSTRING_INDEX(domain, \'.\', -2) as labels',
|
||||
)
|
||||
->selectSum('`hits`', '`values`')
|
||||
->selectSum('hits', 'values')
|
||||
->where([
|
||||
'`podcast_id`' => $podcastId,
|
||||
'`date` >' => $oneYearAgo,
|
||||
'podcast_id' => $podcastId,
|
||||
'date >' => $oneYearAgo,
|
||||
])
|
||||
->groupBy('`labels`')
|
||||
->orderBy('`values`', 'DESC')
|
||||
->groupBy('labels')
|
||||
->orderBy('values', 'DESC')
|
||||
->findAll();
|
||||
cache()->save(
|
||||
"{$podcastId}_analytics_website_by_domain_yearly",
|
||||
$found,
|
||||
600
|
||||
600,
|
||||
);
|
||||
}
|
||||
return $found;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue