├── config ├── routes.yaml ├── controller.yaml ├── commands.yaml └── services.yaml ├── .gitignore ├── .travis.yml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ci.yml ├── public ├── img │ ├── icon_plus.svg │ ├── linkedin-in-brands.svg │ ├── icon_fa_download-solid.svg │ ├── icon_facebook.svg │ ├── icon_twitter.svg │ ├── icon_fa_list-solid.svg │ ├── pdir_logo.svg │ ├── icon_instagram.svg │ └── pdir_icon_socialfeed_plus.svg ├── css │ ├── sf_moderation.css.map │ ├── social_feed.css.map │ ├── backend.min.css │ ├── sf_moderation.min.css │ ├── social_feed.min.css │ ├── sf_moderation.scss │ ├── sf_moderation.css │ ├── backend.css │ ├── social_feed.scss │ └── social_feed.css └── js │ └── imagesloaded.pkgd.min.js ├── .editorconfig ├── .gitlab-ci.yml ├── phpstan.neon ├── phpunit.xml ├── src ├── Model │ └── SocialFeedModel.php ├── Cron │ ├── ImportCronHelperTrait.php │ ├── LinkedInImportCron.php │ ├── RefreshAccessTokenCron.php │ └── InstagramImportCron.php ├── PdirSocialFeedBundle.php ├── DependencyInjection │ ├── Configuration.php │ └── PdirSocialFeedExtension.php ├── Module │ ├── ModuleCustomNewslist.php │ └── NewsCategoriesModule.php ├── Importer │ ├── InstagramRequestCache.php │ ├── Importer.php │ └── NewsImporter.php ├── ContaoManager │ └── Plugin.php ├── Command │ ├── TwitterImportCommand.php │ ├── FacebookImportCommand.php │ ├── InstagramImportCommand.php │ └── LinkedInImportCommand.php ├── EventListener │ └── DataContainer │ │ ├── SetupListener.php │ │ └── SocialFeedListener.php ├── Controller │ ├── LinkedinController.php │ ├── FacebookController.php │ ├── InstagramController.php │ └── ModerateController.php └── SocialFeed │ └── SocialFeedNewsClass.php ├── contao ├── templates │ ├── be │ │ ├── be_socialfeed_setup.html5 │ │ ├── be_sf_moderation_list.html5 │ │ └── be_sf_moderate.html5 │ └── modules │ │ ├── mod_newslist_social_feed.html5 │ │ ├── news_social_feed_extended.html5 │ │ └── news_social_feed.html5 ├── languages │ ├── it │ │ ├── modules.php │ │ ├── tl_module.php │ │ ├── tl_news.php │ │ └── default.php │ ├── de │ │ ├── modules.php │ │ ├── tl_module.php │ │ ├── tl_news.php │ │ └── default.php │ └── en │ │ ├── modules.php │ │ ├── tl_module.php │ │ ├── tl_news.php │ │ └── default.php ├── dca │ ├── tl_news_archive.php │ ├── tl_module.php │ └── tl_news.php └── config │ └── config.php ├── tests └── PdirSocialFeedBundleTest.php ├── ecs.php ├── composer.json ├── README.md ├── LICENSE_FONT_AWESOME └── LICENSE /config/routes.yaml: -------------------------------------------------------------------------------- 1 | pdir_social_feed.controllers: 2 | resource: 3 | path: ../src/Controller/ 4 | namespace: Pdir\SocialFeedBundle\Controller 5 | type: attribute 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | /composer.lock 3 | /vendor/ 4 | 5 | # cypress & node 6 | cypress.json 7 | /node_modules/ 8 | 9 | # PhpUnit 10 | /.idea 11 | /.phpunit.result.cache 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '8.0' 4 | 5 | before_script: 6 | - composer self-update 7 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install --prefer-dist --no-interaction 8 | 9 | script: 10 | - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --log-junit report.xml 11 | - vendor/bin/ecs check src tests 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | 13 | -------------------------------------------------------------------------------- /config/controller.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autoconfigure: true 4 | 5 | Pdir\SocialFeedBundle\Controller\FacebookController: 6 | public: true 7 | arguments: 8 | - '@database_connection' 9 | - '@router' 10 | 11 | Pdir\SocialFeedBundle\Controller\InstagramController: 12 | public: true 13 | 14 | Pdir\SocialFeedBundle\Controller\LinkedinController: 15 | public: true 16 | arguments: 17 | - '@database_connection' 18 | - '@router' 19 | -------------------------------------------------------------------------------- /public/img/icon_plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/linkedin-in-brands.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{php,twig,yml}] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.{html5,svg,min.css,min.js}] 17 | insert_final_newline = false 18 | 19 | [*/src/Resources/contao/**.{css,js,php}] 20 | indent_style = tab 21 | 22 | [*/src/Resources/contao/**.html5] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /public/css/sf_moderation.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["sf_moderation.scss"],"names":[],"mappings":"AAAA;AAGE;EACE;;AAEF;EACE;;AAEF;EACE;;;AAKF;EACE;EACA;EACA;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAEA;EACE;;AAOR;EACE;;AAEA;EACE;EACA;EACA;;;AAKN;EACE;;;AAGF;EACE;IACE;;;AAGJ;EACE;IACE;;;AAKF;EACE;;AACA;EACE;;AAIA;EACE","file":"sf_moderation.css"} -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | cache: 2 | paths: 3 | - vendor/ 4 | 5 | before_script: 6 | # - cp ci/php.ini /usr/local/etc/php/conf.d/test.ini 7 | - COMPOSER_MEMORY_LIMIT=-1 composer install -n --prefer-dist --no-progress --ignore-platform-reqs 8 | 9 | php:8.0: 10 | # This docker image comes with composer and ant. 11 | image: jorge07/alpine-php:8.0-dev 12 | script: 13 | - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --log-junit report.xml 14 | - vendor/bin/ecs check src tests 15 | artifacts: 16 | when: always 17 | reports: 18 | junit: report.xml 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-phpunit/extension.neon 3 | - vendor/phpstan/phpstan-phpunit/rules.neon 4 | - vendor/phpstan/phpstan-symfony/extension.neon 5 | - vendor/phpstan/phpstan-symfony/rules.neon 6 | 7 | parameters: 8 | level: 6 9 | 10 | paths: 11 | - %currentWorkingDirectory%/src 12 | - %currentWorkingDirectory%/tests 13 | 14 | universalObjectCratesClasses: 15 | - Contao\BackendModule 16 | - Contao\ContentElement 17 | - Contao\Model 18 | - Contao\Module 19 | - Contao\Template 20 | 21 | excludePaths: 22 | - src/Module/NewsCategoriesModule.php 23 | -------------------------------------------------------------------------------- /public/img/icon_fa_download-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/icon_facebook.svg: -------------------------------------------------------------------------------- 1 | Element 1 -------------------------------------------------------------------------------- /public/css/social_feed.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["social_feed.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;;AAII;EACE;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;AAIA;EACE;;AAEF;EACE;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAKA;EACE;;AAIJ;EACE;;AAGF;EACE;;AAGF;EACE;;;AAKN;EACE;IACE;IACA;;EAII;IACE;IACA;;EAOF;IACE;IACA","file":"social_feed.css"} -------------------------------------------------------------------------------- /public/img/icon_twitter.svg: -------------------------------------------------------------------------------- 1 | Element 3 -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | ./src 21 | 22 | ./src/Resources 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug description 11 | 12 | 13 | ## Steps to reproduce 14 | 15 | 16 | ## Expected behavior 17 | 18 | 19 | ## Screenshots 20 | 21 | 22 | ## Bundle Version 23 | 24 | 25 | ## Environment 26 | 27 | -------------------------------------------------------------------------------- /public/img/icon_fa_list-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Model/SocialFeedModel.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Model; 22 | 23 | use Contao\Model; 24 | 25 | class SocialFeedModel extends Model 26 | { 27 | protected static $strTable = 'tl_social_feed'; 28 | } 29 | -------------------------------------------------------------------------------- /config/commands.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autoconfigure: true 4 | 5 | _instanceof: 6 | Contao\CoreBundle\Framework\FrameworkAwareInterface: 7 | calls: 8 | - [setFramework, ['@contao.framework']] 9 | 10 | pdir_social_feed.command.facebook: 11 | class: Pdir\SocialFeedBundle\Command\FacebookImportCommand 12 | arguments: 13 | - '@contao.framework' 14 | 15 | pdir_social_feed.command.instagram: 16 | class: Pdir\SocialFeedBundle\Command\InstagramImportCommand 17 | arguments: 18 | - '@contao.framework' 19 | 20 | pdir_social_feed.command.linkedin: 21 | class: Pdir\SocialFeedBundle\Command\LinkedInImportCommand 22 | arguments: 23 | - '@contao.framework' 24 | 25 | pdir_social_feed.command.x: 26 | class: Pdir\SocialFeedBundle\Command\TwitterImportCommand 27 | arguments: 28 | - '@contao.framework' 29 | -------------------------------------------------------------------------------- /contao/templates/be/be_socialfeed_setup.html5: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

(Version: version ?>)

5 |
6 |
7 | 8 |
9 |
10 |

11 |

12 | 13 | 14 |

15 |
16 |
17 |
18 |
19 |
-------------------------------------------------------------------------------- /src/Cron/ImportCronHelperTrait.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Cron; 22 | 23 | trait ImportCronHelperTrait 24 | { 25 | /** 26 | * @var true 27 | */ 28 | private bool $poorManCron = true; 29 | 30 | public function setPoorManCronMode($flag): void 31 | { 32 | if (false === $flag) { 33 | $this->poorManCron = false; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/PdirSocialFeedBundle.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle; 22 | 23 | use Symfony\Component\HttpKernel\Bundle\Bundle; 24 | 25 | /** 26 | * Configures the social feed bundle. 27 | * 28 | * @author Mathias Arzberger 29 | */ 30 | class PdirSocialFeedBundle extends Bundle 31 | { 32 | public function getPath(): string 33 | { 34 | return \dirname(__DIR__); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/img/pdir_logo.svg: -------------------------------------------------------------------------------- 1 | Element 1 -------------------------------------------------------------------------------- /contao/languages/it/modules.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /* 22 | * Module translation 23 | */ 24 | 25 | $GLOBALS['TL_LANG']['MOD']['pdir'] = 'Applicazioni Pdir'; 26 | $GLOBALS['TL_LANG']['MOD']['socialFeedSetup'][0] = 'Social Feed'; 27 | $GLOBALS['TL_LANG']['MOD']['socialFeedSetup'][1] = 'Gestione del modulo Social Feed'; 28 | $GLOBALS['TL_LANG']['MOD']['socialFeed'][0] = 'Social Feed Accounts'; 29 | $GLOBALS['TL_LANG']['MOD']['socialFeed'][1] = 'Configurazione del modulo Social Feed.'; 30 | -------------------------------------------------------------------------------- /tests/PdirSocialFeedBundleTest.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Tests; 22 | 23 | use Pdir\SocialFeedBundle\PdirSocialFeedBundle; 24 | use PHPUnit\Framework\TestCase; 25 | 26 | class PdirSocialFeedBundleTest extends TestCase 27 | { 28 | public function testCanBeInstantiated(): void 29 | { 30 | $bundle = new PdirSocialFeedBundle(); 31 | 32 | $this->assertInstanceOf('Pdir\SocialFeedBundle\PdirSocialFeedBundle', $bundle); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contao/languages/de/modules.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /* 22 | * Module translation 23 | */ 24 | 25 | $GLOBALS['TL_LANG']['MOD']['pdir'] = 'pdir Apps'; 26 | $GLOBALS['TL_LANG']['MOD']['socialFeedSetup'][0] = 'Social Feed Info'; 27 | $GLOBALS['TL_LANG']['MOD']['socialFeedSetup'][1] = 'Verwalten Sie hier das Social Feed Bundle'; 28 | $GLOBALS['TL_LANG']['MOD']['socialFeed'][0] = 'Social Feed Konten'; 29 | $GLOBALS['TL_LANG']['MOD']['socialFeed'][1] = 'Konfigurieren Sie hier die Social Feed Konten.'; 30 | -------------------------------------------------------------------------------- /contao/languages/en/modules.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /* 22 | * Module translation 23 | */ 24 | 25 | $GLOBALS['TL_LANG']['MOD']['pdir'] = 'pdir Apps'; 26 | $GLOBALS['TL_LANG']['MOD']['socialFeedSetup'][0] = 'Social Feed Info'; 27 | $GLOBALS['TL_LANG']['MOD']['socialFeedSetup'][1] = 'Here you can manage the social feed setup.'; 28 | $GLOBALS['TL_LANG']['MOD']['socialFeed'][0] = 'Social Feed accounts'; 29 | $GLOBALS['TL_LANG']['MOD']['socialFeed'][1] = 'Here you can manage the Social Feed accounts.'; 30 | -------------------------------------------------------------------------------- /public/img/icon_instagram.svg: -------------------------------------------------------------------------------- 1 | Element 2 -------------------------------------------------------------------------------- /contao/dca/tl_news_archive.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | use Contao\ArrayUtil; 22 | 23 | $table = 'tl_news_archive'; 24 | 25 | /* 26 | * Add global operations 27 | */ 28 | ArrayUtil::arrayInsert($GLOBALS['TL_DCA'][$table]['list']['global_operations'], 0, [ 29 | 'socialFeedAccounts' => [ 30 | 'label' => &$GLOBALS['TL_LANG']['MSC']['socialFeedAccounts'], 31 | 'href' => 'do=socialFeed', 32 | 'class' => 'header_socialFeedAccounts', 33 | 'icon' => '/bundles/pdirsocialfeed/img/icon_fa_list-solid.svg', 34 | 'attributes' => 'onclick="Backend.getScrollOffset()"', 35 | ], 36 | ]); 37 | -------------------------------------------------------------------------------- /src/Cron/LinkedInImportCron.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Cron; 22 | 23 | use Contao\CoreBundle\Framework\ContaoFramework; 24 | use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob; 25 | use Pdir\SocialFeedBundle\Importer\LinkedIn; 26 | 27 | #[AsCronJob('minutely')] 28 | class LinkedInImportCron 29 | { 30 | public function __construct(private ContaoFramework $framework) 31 | { 32 | } 33 | public function __invoke(): void 34 | { 35 | $this->framework->initialize(); 36 | 37 | // run LinkedIn import 38 | $importer = new LinkedIn(); 39 | $importer->setPoorManCronMode(true); 40 | $importer->import(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | 20 | @author Philipp Seibt 21 | @author pdir GmbH 22 | 23 | For the full copyright and license information, please view the LICENSE 24 | file that was distributed with this source code. 25 | EOF; 26 | 27 | return static function (ContainerConfigurator $containerConfigurator): void { 28 | $containerConfigurator->import(__DIR__.'/vendor/contao/easy-coding-standard/config/set/contao.php'); 29 | 30 | $parameters = $containerConfigurator->parameters(); 31 | $parameters->set(Option::LINE_ENDING, "\n"); 32 | 33 | $services = $containerConfigurator->services(); 34 | $services 35 | ->set(HeaderCommentFixer::class) 36 | ->call('configure', [[ 37 | 'header' => $GLOBALS['ecsHeader'], 38 | ]]) 39 | ; 40 | }; 41 | -------------------------------------------------------------------------------- /contao/templates/modules/mod_newslist_social_feed.html5: -------------------------------------------------------------------------------- 1 | extend('block_unsearchable'); ?> 2 | 3 | block('content'); ?> 4 | 5 | articles)): ?> 6 |

empty ?>

7 | 8 | 11 | pagination ?> 12 | 13 | 14 | endblock(); ?> 15 | 16 | sfMasonry): ?> 17 | "; ?> 18 | "; ?> 19 | 21 | jQuery(document).ready(function($) { 22 | var grid = $('.social_feed_container').imagesLoaded( function() { 23 | // init Masonry after all images have loaded 24 | grid.masonry({ 25 | itemSelector:'.social_feed_element' 26 | }); 27 | }); 28 | }); 29 | "; 30 | ?> 31 | lazyload): ?> 32 | $('.social_feed_element img').on('load', function () {grid.masonry({itemSelector:'.social_feed_element'});});"; ?> 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\DependencyInjection; 22 | 23 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 24 | use Symfony\Component\Config\Definition\ConfigurationInterface; 25 | 26 | class Configuration implements ConfigurationInterface 27 | { 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function getConfigTreeBuilder(): TreeBuilder 32 | { 33 | $treeBuilder = new TreeBuilder('pdir_socialfeed'); 34 | 35 | if (method_exists($treeBuilder, 'getRootNode')) { 36 | $rootNode = $treeBuilder->getRootNode(); 37 | } else { 38 | // Backwards compatibility 39 | $rootNode = $treeBuilder->root('pdir_socialfeed'); 40 | } 41 | 42 | $rootNode 43 | ->children() 44 | ->integerNode('cache_ttl')->defaultValue(3600)->end() 45 | ->end() 46 | ; 47 | 48 | return $treeBuilder; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Module/ModuleCustomNewslist.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Module; 22 | 23 | use Contao\LayoutModel; 24 | use Contao\ModuleNewsList; 25 | 26 | class ModuleCustomNewslist extends ModuleNewsList 27 | { 28 | protected function compile() 29 | { 30 | parent::compile(); 31 | 32 | $this->Template->sfMasonry = $this->pdir_sf_enableMasonry; 33 | $this->Template->sfColumns = ' '.$this->pdir_sf_columns; 34 | 35 | // only used if the contao speed bundle is installed and the js_lazyload template is activated (https://github.com/heimrichhannot/contao-speed-bundle) 36 | $this->Template->lazyload = false; 37 | $layout = LayoutModel::findByPk($GLOBALS['objPage']->layout); 38 | 39 | if (null !== $layout->scripts && strpos($layout->scripts, 'lazyload')) { 40 | $this->Template->lazyload = true; 41 | } 42 | 43 | $GLOBALS['TL_CSS']['social_feed'] = 'bundles/pdirsocialfeed/css/social_feed.min.css|static'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Module/NewsCategoriesModule.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Module; 22 | 23 | use Codefog\NewsCategoriesBundle\FrontendModule\NewsListModule; 24 | use Contao\LayoutModel; 25 | 26 | class NewsCategoriesModule extends NewsListModule 27 | { 28 | protected function compile() 29 | { 30 | parent::compile(); 31 | 32 | $this->Template->sfMasonry = $this->pdir_sf_enableMasonry; 33 | $this->Template->sfColumns = ' '.$this->pdir_sf_columns; 34 | 35 | // only used if the contao speed bundle is installed and the js_lazyload template is activated (https://github.com/heimrichhannot/contao-speed-bundle) 36 | $this->Template->lazyload = false; 37 | $layout = LayoutModel::findByPk($GLOBALS['objPage']->layout); 38 | 39 | if (null !== $layout->scripts && strpos($layout->scripts, 'lazyload')) { 40 | $this->Template->lazyload = true; 41 | } 42 | 43 | $GLOBALS['TL_CSS']['social_feed'] = 'bundles/pdirsocialfeed/css/social_feed.min.css|static'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contao/languages/en/tl_module.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /* 22 | * Module translation 23 | */ 24 | 25 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_settings_legend'] = 'Social Feed Settings'; 26 | 27 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_text_length'] = ['Maximum text length', 'Here you can enter the maximum text length. Enter 0 to show all the text.']; 28 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableMasonry'] = ['Activate Masonry', 'Masonry works by placing elements in optimal position based on available vertical space, sort of like a mason fitting stones in a wall.']; 29 | $GLOBALS['TL_LANG']['tl_module']['column1'] = 'single column'; 30 | $GLOBALS['TL_LANG']['tl_module']['columns2'] = 'two columns'; 31 | $GLOBALS['TL_LANG']['tl_module']['columns3'] = 'three columns'; 32 | $GLOBALS['TL_LANG']['tl_module']['columns4'] = 'four coumns'; 33 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_columns'] = ['Number of columns', 'Here you can choose the number of columns for each element.']; 34 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableImages'] = ['Show images', 'When active, images are displayed.']; 35 | -------------------------------------------------------------------------------- /src/DependencyInjection/PdirSocialFeedExtension.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\DependencyInjection; 22 | 23 | use Pdir\SocialFeedBundle\Importer\InstagramRequestCache; 24 | use Symfony\Component\Config\FileLocator; 25 | use Symfony\Component\DependencyInjection\ContainerBuilder; 26 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 27 | use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; 28 | 29 | class PdirSocialFeedExtension extends ConfigurableExtension 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void 35 | { 36 | $loader = new YamlFileLoader( 37 | $container, 38 | new FileLocator(__DIR__.'/../../config') 39 | ); 40 | 41 | $loader->load('commands.yaml'); 42 | $loader->load('controller.yaml'); 43 | $loader->load('services.yaml'); 44 | 45 | $container->getDefinition(InstagramRequestCache::class)->setArgument(1, (int) $mergedConfig['cache_ttl']); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contao/languages/it/tl_module.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /* 22 | * Module translation 23 | */ 24 | 25 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_settings_legend'] = 'Impostazioni del Social Feed'; 26 | 27 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_text_length'] = ['Lunghezza massima del testo', 'Inserisci qui la lunghezza massima del testo dei post. Inseriere 0 per visualizzare il testo completo.']; 28 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableMasonry'] = ['Attiva Masonry', 'Attiva funzione Masonry. Gli elementi vengono posizionati in una posizione ottimale in base alla posizione verticale disponibile.']; 29 | $GLOBALS['TL_LANG']['tl_module']['column1'] = 'Colonna singola'; 30 | $GLOBALS['TL_LANG']['tl_module']['columns2'] = 'Due colonne'; 31 | $GLOBALS['TL_LANG']['tl_module']['columns3'] = 'Tre colonne'; 32 | $GLOBALS['TL_LANG']['tl_module']['columns4'] = 'Quattro colonne'; 33 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_columns'] = ['Numero di colonne', 'Inserisci il numero di colonne per la visualizzazione.']; 34 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableImages'] = ['Visualizza le immagini', 'Se questa opzione è attiva, vengono visualizzate le immagini.']; 35 | -------------------------------------------------------------------------------- /contao/languages/de/tl_module.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /* 22 | * Module translation 23 | */ 24 | 25 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_settings_legend'] = 'Social Feed Einstellungen'; 26 | 27 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_text_length'] = ['Maximale Textlänge', 'Bitte geben Sie hier die maximale Textlänge der Posts an. Geben Sie 0 an, um den kompletten Text anzuzeigen.']; 28 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableMasonry'] = ['Masonry aktivieren', 'Elemente werden basierend auf der verfügbaren vertikalen Position in einer optimalen Position platziert, ähnlich wie bei einem Maurer, der Steine in einer Wand einpasst.']; 29 | $GLOBALS['TL_LANG']['tl_module']['column1'] = 'Einspaltig'; 30 | $GLOBALS['TL_LANG']['tl_module']['columns2'] = 'Zweispaltig'; 31 | $GLOBALS['TL_LANG']['tl_module']['columns3'] = 'Dreispaltig'; 32 | $GLOBALS['TL_LANG']['tl_module']['columns4'] = 'Vierspaltig'; 33 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_columns'] = ['Anzahl der Spalten', 'Geben Sie hier die gewünschte Darstellung der Elemente aus.']; 34 | $GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableImages'] = ['Bilder anzeigen', 'Wenn aktiv, werden die Bilder dargestellt.']; 35 | -------------------------------------------------------------------------------- /contao/templates/modules/news_social_feed_extended.html5: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Importer/InstagramRequestCache.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Importer; 22 | 23 | use Symfony\Component\Filesystem\Filesystem; 24 | 25 | class InstagramRequestCache 26 | { 27 | /** 28 | * @var Filesystem 29 | */ 30 | private $fs; 31 | 32 | /** 33 | * @var int 34 | */ 35 | private $cacheTtl; 36 | 37 | /** 38 | * @var string 39 | */ 40 | private $projectDir; 41 | 42 | /** 43 | * InstagramRequestCache constructor. 44 | */ 45 | public function __construct(Filesystem $fs, int $cacheTtl, string $projectDir) 46 | { 47 | $this->fs = $fs; 48 | $this->cacheTtl = $cacheTtl; 49 | $this->projectDir = $projectDir; 50 | } 51 | 52 | /** 53 | * Get the cache dir. 54 | */ 55 | public function getCacheDir(int $socialFeedId = null): ?string 56 | { 57 | return $this->projectDir.'/var/cache/instagram/'.($socialFeedId ?? '_'); 58 | } 59 | 60 | /** 61 | * Get the cache TTL. 62 | */ 63 | public function getCacheTtl(): int 64 | { 65 | return $this->cacheTtl; 66 | } 67 | 68 | /** 69 | * Purge the cache dir. 70 | */ 71 | public function purge(string $dir): void 72 | { 73 | $this->fs->remove($dir); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /public/css/backend.min.css: -------------------------------------------------------------------------------- 1 | #tl_navigation .group-pdir{background:url(/bundles/pdirsocialfeed/img/pdir_logo.svg)0 0 no-repeat;background-size:18px 18px}.be_socialfeed_setup a{color:#7abfbc}.be_socialfeed_setup .right{float:right;width:40%;margin:15px 0}.be_socialfeed_setup .left{float:left;width:50%;margin:15px 0}.be_socialfeed_setup .feed .item,.be_socialfeed_setup .logo{margin-bottom:5px}.be_socialfeed_setup h2{margin-bottom:10px}.be_socialfeed_setup hr{width:100%;margin:20px auto}.be_socialfeed_setup .devlog{margin:10px 0}.be_socialfeed_setup .link-list li{padding:5px 0}.be_socialfeed_setup .feed .item span{padding-right:10px;color:#7abfbc}.be_socialfeed_setup .button{float:left;text-align:center;margin-right:25px}.be_socialfeed_setup .kreis{width:60px;padding:13px;box-sizing:border-box;margin:auto}.be_socialfeed_setup small{color:#ccc}.be_socialfeed_setup .api-status .blink{width:15px;height:15px;padding:10px;box-sizing:border-box;border-radius:10px;background:#acda3d;display:inline-block;position:relative;top:5px;margin-left:5px}.be_socialfeed_setup .api-status .blink.red{background:#cc3d09}.be_socialfeed_setup .debug-info{background:#ccc;color:#fff;padding:10px;margin-top:15px}.be_socialfeed_setup .plus-logo{float:right}.be_socialfeed_setup .benefit li:before,.be_socialfeed_setup .high-plus{content:"";background:url(/bundles/pdirsocialfeed/img/icon_plus.svg)0 0 no-repeat;background-size:12px;display:inline-block;width:12px;height:12px;margin-left:5px}.be_socialfeed_setup .benefit{margin-top:12px;line-height:18px;list-style:none}.be_socialfeed_setup .benefit li{margin-left:20px}.be_socialfeed_setup .benefit li:before{position:absolute;margin-left:-19px;margin-top:3px}.selectAll{margin:15px 15px -5px;display:inline-block}.selectAll input{margin-right:5px}a.header_socialFeedAccounts{background-size:16px}.header_sf_moderate{margin-left:34px}.header_sf_moderate:before{content:"";position:absolute;width:16px;height:16px;margin-left:-22px;background:url(/bundles/pdirsocialfeed/img/icon_fa_download-solid.svg)0 0 no-repeat} -------------------------------------------------------------------------------- /contao/templates/modules/news_social_feed.html5: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/css/sf_moderation.min.css: -------------------------------------------------------------------------------- 1 | .mod_sf_moderate h1{width:80%}.mod_sf_moderate h2.sub_headline{margin:3px 15px}.mod_sf_moderate #tl_buttons{float:right}.be_sf_morderation_list .list{display:flex;flex-wrap:wrap;list-style:none;padding:0 5px}.be_sf_morderation_list .list-item{box-sizing:border-box;flex:0 0 auto;padding:1em;margin:.5em;width:calc(33.3% - 1em);border:1px solid #ccc;position:relative}.be_sf_morderation_list .list-item .list-content{background-color:#fff;width:100%}.be_sf_morderation_list .list-item .list-content p{margin-top:10px}.be_sf_morderation_list .list-item .checkbox-container{display:block;position:absolute;right:0;top:10px;padding-left:35px;margin-bottom:12px;cursor:pointer;font-size:22px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.be_sf_morderation_list .list-item .checkbox-container input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}.be_sf_morderation_list .list-item .checkbox-container .checkmark{position:absolute;top:0;left:0;height:25px;width:25px;background-color:#eee}.be_sf_morderation_list .list-item .checkbox-container .checkmark:after{content:"";position:absolute;display:none;left:9px;top:5px;width:5px;height:10px;border:solid #fff;border-width:0 3px 3px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.be_sf_morderation_list .list-item .checkbox-container:hover input~.checkmark{background-color:#ccc}.be_sf_morderation_list .list-item .checkbox-container input:checked~.checkmark{background-color:#2196f3}.be_sf_morderation_list .list-item .checkbox-container input:checked~.checkmark:after{display:block}.be_sf_morderation_list .tl_formbody_submit{position:relative}.be_sf_morderation_list .tl_formbody_submit .tl_submit_container{position:fixed;bottom:0;width:100%}a.header_sf_moderate{background-size:16px}@media all and (min-width:40em){.be_sf_morderation_list .list-item{wi2dth:50%}}@media all and (min-width:60em){.be_sf_morderation_list .list-item{wi2dth:33.33%}}html[data-color-scheme=dark] .be_sf_morderation_list .list-item{border:1px solid var(--form-border)}html[data-color-scheme=dark] .be_sf_morderation_list .list-item .list-content{background:var(--content-bg)}html[data-color-scheme=dark] .be_sf_morderation_list .list-item .checkbox-container .checkmark{border:solid var(--form-border)} -------------------------------------------------------------------------------- /contao/languages/en/tl_news.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | $GLOBALS['TL_LANG']['tl_news']['sf_moderate'] = ['Import moderated', 'Here you can import posts from Instagram moderated.']; 22 | 23 | $GLOBALS['TL_LANG']['tl_news']['pdir_sf_settings_legend'] = 'Social Feed Settings'; 24 | $GLOBALS['TL_LANG']['tl_news']['social_feed_id'] = ['Social Feed Post ID', 'Here you can specify the ID of the social feed.']; 25 | $GLOBALS['TL_LANG']['tl_news']['social_feed_type'] = ['Social Feed Type', 'Here you can specify the type of social feed.']; 26 | $GLOBALS['TL_LANG']['tl_news']['social_feed_account'] = ['Social Feed Account', 'Here you can specify the account of the social feed.']; 27 | $GLOBALS['TL_LANG']['tl_news']['social_feed_account_picture'] = ['Social Feed Profile Picture', 'Here you can specify the profile picture of the social feed.']; 28 | 29 | $GLOBALS['TL_LANG']['tl_news']['moderate'] = ['Get Feed', 'Moderate Social Feed Posts']; 30 | $GLOBALS['TL_LANG']['tl_news_moderate']['moderate'] = ['Get Feed', 'Moderate Social Feed Posts']; 31 | $GLOBALS['TL_LANG']['tl_news_moderate']['moderateHint'] = 'Only Instagram is currently available for moderated import. Use cron import for other services, you can find the settings in the Social Feed accounts.'; 32 | $GLOBALS['TL_LANG']['tl_news_moderate']['account'] = ['Social Feed Account', 'Here you can specify the account for moderation.']; 33 | $GLOBALS['TL_LANG']['tl_news_moderate']['number_posts'] = ['Number of posts', 'Here you can limit the number of posts to be retrieved']; 34 | $GLOBALS['TL_LANG']['tl_news_moderate']['selectAll'] = 'Select all'; 35 | $GLOBALS['TL_LANG']['tl_news_moderate']['open'] = 'Open'; 36 | $GLOBALS['TL_LANG']['tl_news_moderate']['importEntry'] = 'Import entries'; 37 | -------------------------------------------------------------------------------- /src/ContaoManager/Plugin.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\ContaoManager; 22 | 23 | use Codefog\NewsCategoriesBundle\CodefogNewsCategoriesBundle; // @phpstan-ignore-line 24 | use Contao\CoreBundle\ContaoCoreBundle; 25 | use Contao\ManagerPlugin\Bundle\BundlePluginInterface; 26 | use Contao\ManagerPlugin\Bundle\Config\BundleConfig; 27 | use Contao\ManagerPlugin\Bundle\Parser\ParserInterface; 28 | use Contao\ManagerPlugin\Routing\RoutingPluginInterface; 29 | use Contao\NewsBundle\ContaoNewsBundle; 30 | use Symfony\Component\Config\Loader\LoaderResolverInterface; 31 | use Symfony\Component\HttpKernel\KernelInterface; 32 | use Symfony\Component\Routing\RouteCollection; 33 | use Pdir\SocialFeedBundle\PdirSocialFeedBundle; 34 | 35 | class Plugin implements BundlePluginInterface,RoutingPluginInterface 36 | { 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function getBundles(ParserInterface $parser) 41 | { 42 | return [ 43 | BundleConfig::create(PdirSocialFeedBundle::class) 44 | ->setLoadAfter( 45 | [ 46 | ContaoCoreBundle::class, 47 | ContaoNewsBundle::class, 48 | CodefogNewsCategoriesBundle::class 49 | ] 50 | ) // @phpstan-ignore-line 51 | ->setReplace(['socialfeedbundle']), 52 | ]; 53 | } 54 | public function getRouteCollection(LoaderResolverInterface $resolver, KernelInterface $kernel): RouteCollection|null 55 | { 56 | return $resolver 57 | ->resolve(__DIR__.'/../../config/routes.yaml') 58 | ->load(__DIR__.'/../../config/routes.yaml') 59 | ; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contao/config/config.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | use Contao\System; 22 | use Pdir\SocialFeedBundle\Module\ModuleCustomNewslist; 23 | use Pdir\SocialFeedBundle\Module\NewsCategoriesModule; // @phpstan-ignore-line 24 | use Symfony\Component\HttpFoundation\Request; 25 | 26 | /* 27 | * Backend modules 28 | */ 29 | if (!isset($GLOBALS['BE_MOD']['pdir']) || !\is_array($GLOBALS['BE_MOD']['pdir'])) { 30 | \array_splice($GLOBALS['BE_MOD'], 1,0, ['pdir' => []]); 31 | } 32 | 33 | $GLOBALS['TL_HOOKS']['parseArticles'][] = ['Pdir\SocialFeedBundle\SocialFeed\SocialFeedNewsClass', 'parseNews']; 34 | 35 | $assetsDir = 'bundles/pdirsocialfeed'; 36 | 37 | $GLOBALS['BE_MOD']['pdir']['socialFeed'] = [ 38 | 'tables' => ['tl_social_feed'], 39 | ]; 40 | 41 | $GLOBALS['BE_MOD']['content']['news']['moderate'] = ['pdir_social_feed_moderate.controller', 'run']; 42 | 43 | /* 44 | * Models 45 | */ 46 | 47 | $GLOBALS['TL_MODELS']['tl_social_feed'] = 'Pdir\SocialFeedBundle\Model\SocialFeedModel'; 48 | 49 | /* 50 | * Frontend modules 51 | */ 52 | $GLOBALS['FE_MOD']['news']['newslist'] = ModuleCustomNewslist::class; 53 | 54 | if (\str_contains($GLOBALS['FE_MOD']['news']['newslist'], 'Codefog\NewsCategoriesBundle')) { 55 | $GLOBALS['FE_MOD']['news']['newslist'] = NewsCategoriesModule::class; // @phpstan-ignore-line 56 | } 57 | 58 | /* 59 | * CSS for Backend 60 | */ 61 | $request = System::getContainer()->get('request_stack')->getCurrentRequest(); 62 | $scopeMatcher = System::getContainer()->get('contao.routing.scope_matcher'); 63 | 64 | if ($request && $scopeMatcher->isBackendRequest($request)) { 65 | $GLOBALS['TL_CSS'][] = $assetsDir.'/css/sf_moderation.min.css|static'; 66 | $GLOBALS['TL_CSS'][] = $assetsDir.'/css/backend.min.css|static'; 67 | } 68 | -------------------------------------------------------------------------------- /contao/templates/be/be_sf_moderation_list.html5: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 7 |
8 | 9 |
    10 | arr as $key => $item): ?> 11 |
  • 12 |
    13 | 14 |

    15 | 16 |

    17 |
    18 |
    19 |

    20 | 24 |
    25 |
    26 |
  • 27 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 | 56 | -------------------------------------------------------------------------------- /contao/languages/de/tl_news.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | $GLOBALS['TL_LANG']['tl_news']['sf_moderate'] = ['Import moderieren', 'Hier kannst du Beiträge von Instagram moderiert importieren.']; 22 | 23 | $GLOBALS['TL_LANG']['tl_news']['pdir_sf_settings_legend'] = 'Social Feed Einstellungen'; 24 | $GLOBALS['TL_LANG']['tl_news']['social_feed_id'] = ['Social Feed Post-ID', 'Hier können Sie die ID des Social Feeds angeben.']; 25 | $GLOBALS['TL_LANG']['tl_news']['social_feed_type'] = ['Social Feed Typ', 'Hier können Sie den Typ des Social Feeds angeben.']; 26 | $GLOBALS['TL_LANG']['tl_news']['social_feed_account'] = ['Social Feed Account', 'Hier können Sie den Account des Social Feeds angeben.']; 27 | $GLOBALS['TL_LANG']['tl_news']['social_feed_account_picture'] = ['Social Feed Profilbild', 'Hier können Sie das Profilbild des social Feeds angeben.']; 28 | 29 | $GLOBALS['TL_LANG']['tl_news']['moderate'] = ['Feed abrufen', 'Social Feed Einträge moderieren']; 30 | $GLOBALS['TL_LANG']['tl_news_moderate']['moderate'] = ['Feed abrufen', 'Social Feed Einträge moderieren']; 31 | $GLOBALS['TL_LANG']['tl_news_moderate']['moderateHint'] = 'Für den moderierten Import steht derzeit nur Instagram zur Verfügung. Nutze für andere Dienste den Import per Cron, die Einstellungen findest du in den Social Feed Konten.'; 32 | $GLOBALS['TL_LANG']['tl_news_moderate']['account'] = ['Social Feed Konto', 'Hier können Sie den Account für die Moderation angeben.']; 33 | $GLOBALS['TL_LANG']['tl_news_moderate']['number_posts'] = ['Anzahl der Posts', 'Hier können Sie die Anzahl der Posts, die abgerufen werden sollen, begrenzen.']; 34 | $GLOBALS['TL_LANG']['tl_news_moderate']['selectAll'] = 'Alles auswählen'; 35 | $GLOBALS['TL_LANG']['tl_news_moderate']['open'] = 'Öffnen'; 36 | $GLOBALS['TL_LANG']['tl_news_moderate']['importEntry'] = 'Einträge importieren'; 37 | -------------------------------------------------------------------------------- /contao/languages/it/tl_news.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | $GLOBALS['TL_LANG']['tl_news']['sf_moderate'] = ['Importa moderati', 'Qui è possibile importare i post di Instagram moderati.']; 22 | 23 | $GLOBALS['TL_LANG']['tl_news']['pdir_sf_settings_legend'] = 'Impostazioni Social Feed'; 24 | $GLOBALS['TL_LANG']['tl_news']['social_feed_id'] = ['Social Feed Post-ID', 'Qui è possibile specificare l\'ID del social feed']; 25 | $GLOBALS['TL_LANG']['tl_news']['social_feed_type'] = ['Tipo di Social Feed', 'Qui è possibile specificare il tipo di social feed.']; 26 | $GLOBALS['TL_LANG']['tl_news']['social_feed_account'] = ['Social Feed Account', 'Qui è possibile specificare l\'account del social feed.']; 27 | $GLOBALS['TL_LANG']['tl_news']['social_feed_account_picture'] = ['Immagine del profilo Social Feed', 'Qui è possibile specificare l\'immagine del profilo del social feed']; 28 | 29 | $GLOBALS['TL_LANG']['tl_news']['moderate'] = ['Recupera alimentazione', 'Moderare le voci dei social feed.']; 30 | $GLOBALS['TL_LANG']['tl_news_moderate']['moderate'] = ['Importa il Social Feed', 'Modera l\'importazione del Social Feed']; 31 | $GLOBALS['TL_LANG']['tl_news_moderate']['moderateHint'] = 'Solo Instagram è attualmente disponibile per l\'importazione moderata. Per gli altri servizi, utilizzare l\'importazione tramite cron; le impostazioni sono disponibili negli account dei feed sociali.'; 32 | $GLOBALS['TL_LANG']['tl_news_moderate']['account'] = ['Account Social Feed', 'Qui è possibile specificare l\'account da moderare']; 33 | $GLOBALS['TL_LANG']['tl_news_moderate']['number_posts'] = ['Numero di posti', 'Qui è possibile limitare il numero di post da recuperare.']; 34 | $GLOBALS['TL_LANG']['tl_news_moderate']['selectAll'] = 'Seleziona tutti'; 35 | $GLOBALS['TL_LANG']['tl_news_moderate']['open'] = 'Aprire'; 36 | $GLOBALS['TL_LANG']['tl_news_moderate']['importEntry'] = 'Voci di importazione'; 37 | -------------------------------------------------------------------------------- /contao/dca/tl_module.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | /* 22 | * Add palette to tl_module 23 | */ 24 | 25 | $GLOBALS['TL_DCA']['tl_module']['palettes']['newslist'] = str_replace('cssID', 'cssID;{pdir_sf_settings_legend},pdir_sf_text_length,pdir_sf_columns,pdir_sf_enableMasonry,pdir_sf_enableImages', $GLOBALS['TL_DCA']['tl_module']['palettes']['newslist']); 26 | 27 | $GLOBALS['TL_DCA']['tl_module']['fields']['pdir_sf_text_length'] = [ 28 | 'label' => &$GLOBALS['TL_LANG']['tl_module']['pdir_sf_text_length'], 29 | 'exclude' => true, 30 | 'inputType' => 'text', 31 | 'eval' => [ 32 | 'submitOnChange' => true, 33 | 'tl_class' => 'w50', 34 | ], 35 | 'sql' => "int(10) unsigned NOT NULL default '0'", 36 | ]; 37 | 38 | $GLOBALS['TL_DCA']['tl_module']['fields']['pdir_sf_enableMasonry'] = [ 39 | 'label' => &$GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableMasonry'], 40 | 'exclude' => true, 41 | 'inputType' => 'checkbox', 42 | 'eval' => [ 43 | 'submitOnChange' => true, 44 | 'tl_class' => 'w50 m12', 45 | ], 46 | 'sql' => "char(1) NOT NULL default ''", 47 | ]; 48 | 49 | $GLOBALS['TL_DCA']['tl_module']['fields']['pdir_sf_columns'] = [ 50 | 'label' => &$GLOBALS['TL_LANG']['tl_module']['pdir_sf_columns'], 51 | 'exclude' => true, 52 | 'inputType' => 'select', 53 | 'eval' => [ 54 | 'tl_class' => 'w50', 55 | ], 56 | 'options' => ['column1', 'columns2', 'columns3', 'columns4'], 57 | 'reference' => &$GLOBALS['TL_LANG']['tl_module'], 58 | 'sql' => "varchar(64) NOT NULL default 'columns3'", 59 | ]; 60 | 61 | $GLOBALS['TL_DCA']['tl_module']['fields']['pdir_sf_enableImages'] = [ 62 | 'label' => &$GLOBALS['TL_LANG']['tl_module']['pdir_sf_enableImages'], 63 | 'exclude' => true, 64 | 'inputType' => 'checkbox', 65 | 'eval' => [ 66 | 'submitOnChange' => true, 67 | 'tl_class' => 'w50 m12', 68 | ], 69 | 'sql' => "char(1) NOT NULL default '1'", 70 | ]; 71 | -------------------------------------------------------------------------------- /contao/templates/be/be_sf_moderate.html5: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | 10 |
11 | 12 | block('headline'); ?> 13 | headline): ?> 14 |

headline ?>

15 | 16 | 17 | endblock(); ?> 18 | 19 |

20 | 21 |
22 |

23 | message ?> 24 |
25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 |

33 | 38 |

39 |
40 | 41 |
42 |

43 | 44 |

45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 | 54 |
55 |
56 | 57 | moderationList): ?> 58 | moderationList ?> 59 | 60 | 61 |
62 |
-------------------------------------------------------------------------------- /contao/languages/en/default.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['importMessage'] = '%s entries were imported.'; 22 | $GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInSubject'] = 'LinkedIn Access Token Erinnerung'; 23 | $GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInHtml'] = 'Hello Admin,

The LinkedIn Access Token on the website %s for the account %s needs to be regenerated. To do this, log in to the Contao backend, open the settings of the social feed account, select the checkbox "Generate Access Token" and save. Afterwards, you only have to allow the app access and the Access Token will be generated again. This process must be repeated every year. Powered by Social Feed+ Bundle'; 24 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['noItems'] = 'No items where found to moderate.'; 25 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['headline'] = 'News › Social Feed › Moderate › Archive '; 26 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['facebookNotSupported'] = 'Facebook posts can\'t yet be imported into the news archive via the manual import. Please set up a cronjob in the social feed account and use the automatic import. Currently, only Instagram posts can be imported via the manual import.'; 27 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['twitterNotSupported'] = 'X posts can\'t yet be imported into the news archive via the manual import. Please set up a cronjob in the social feed account and use the automatic import. Currently, only Instagram posts can be imported via the manual import.'; 28 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['linkedInNotSupported'] = 'LinkedIn posts can\'t yet be imported into the news archive via the manual import. Please set up a cronjob in the social feed account and use the automatic import. Currently, only Instagram posts can be imported via the manual import.'; 29 | $GLOBALS['TL_LANG']['BE_MOD']['psfNoAccessToken'] = 'No access token given.'; 30 | $GLOBALS['TL_LANG']['MSC']['pdirSocialFeedNoTitel'] = 'No title'; 31 | $GLOBALS['TL_LANG']['MSC']['socialFeedAccounts'] = 'Social Feed accounts'; 32 | -------------------------------------------------------------------------------- /contao/languages/de/default.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['importMessage'] = 'Es wurden %s Einträge importiert.'; 22 | $GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInSubject'] = 'LinkedIn Access Token Erinnerung'; 23 | $GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInHtml'] = 'Hallo Admin,

Der LinkedIn Access Token auf der Webseite %s für den Account %s muss neu generiert werden. Melde dich dafür im Contao Backend an, rufe die Einstellungen des Social Feed Kontos auf, wähle die Checkbox "Generiere Access Token" aus und speichere. Anschließend musst du nur noch der App den Zugriff erlauben und der Access Token wird neu generiert. Dieser Vorgang muss jedes Jahr wiederholt werden. Powered by Social Feed+ Bundle'; 24 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['noItems'] = 'Es wurden keine Einträge gefunden.'; 25 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['headline'] = 'Nachrichten › Social Feed › Moderieren › Archiv '; 26 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['facebookNotSupported'] = 'Facebook-Posts können über den manuellen Import derzeit noch nicht ins News-Archiv importiert werden. Bitte stelle im Social Feed Account einen Cronjob ein und nutze den automatischen Import. Aktuell können nur Instagram-Posts über den manuellen Import importiert werden.'; 27 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['twitterNotSupported'] = 'X-Posts können über den manuellen Import derzeit noch nicht ins News-Archiv importiert werden. Bitte stelle im Social Feed Account einen Cronjob ein und nutze den automatischen Import. Aktuell können nur Instagram-Posts über den manuellen Import importiert werden.'; 28 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['linkedInNotSupported'] = 'LinkedIn-Posts können über den manuellen Import derzeit noch nicht ins News-Archiv importiert werden. Bitte stelle im Social Feed Account einen Cronjob ein und nutze den automatischen Import. Aktuell können nur Instagram-Posts über den manuellen Import importiert werden.'; 29 | $GLOBALS['TL_LANG']['BE_MOD']['psfNoAccessToken'] = 'Kein Zugriffstoken angegeben.'; 30 | $GLOBALS['TL_LANG']['MSC']['pdirSocialFeedNoTitel'] = 'Kein Titel'; 31 | $GLOBALS['TL_LANG']['MSC']['socialFeedAccounts'] = 'Social Feed Konten'; 32 | -------------------------------------------------------------------------------- /public/css/social_feed.min.css: -------------------------------------------------------------------------------- 1 | .social_feed_element{overflow:visible;padding:0;width:100%}.social_feed_element.extended .inner a{display:inline;padding:0;color:#1b95e0}.social_feed_element.extended .inner a:hover,.social_feed_element.extended a.more:hover{text-decoration:underline}.social_feed_element.extended .inner>figure{padding:0}.social_feed_element.extended a.more{padding:0;font-size:14px;color:#1b95e0;font-weight:700;display:block}.social_feed_element.extended .inner{padding:0 20px 20px}.social_feed_element.extended p:first-child{margin-top:0}.social_feed_element.extended p:last-child{margin-bottom:0}.social_feed_element .inner{background:#f2f2f2;padding:0;margin:30px 10px 10px;position:relative;word-break:break-word;border-top:5px solid #c1c1c1}.social_feed_element .inner a{padding:0 20px;display:block}.social_feed_element .inner>figure{padding:0 20px}.social_feed_element .ce_text{padding:15px 0;color:#333;margin:0}.social_feed_element .icon{position:static;text-indent:0}.social_feed_element .icon img{position:absolute;top:-20px;border-radius:100%;max-width:50px}.social_feed_element .icon .image-wrapper:not(.loaded){height:0!important}.social_feed_element .info{margin:0;padding:10px 0 0;text-align:right;font-size:14px;color:#989898;display:flex;align-items:center;justify-content:flex-end}.social_feed_element .info img{width:15px;margin-left:10px}.social_feed_element .title{margin:10px 0;font-size:16px;font-weight:700;color:#333}.social_feed_element a{color:#333;text-decoration:none}.social_feed_element .ce_text{font-size:14px}.social_feed_element .fa{font-size:18px;padding-left:5px}.social_feed_element .image_container>a{padding:0}.social_feed_container{width:calc(100% + 30px);margin-left:-15px;font-family:sans-serif}.social_feed_container:not(.masonry){display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.social_feed_container:not(.masonry).columns2 .social_feed_element{-ms-flex:0 0 50%;flex:0 0 50%}.social_feed_container:not(.masonry).columns3 .social_feed_element{-ms-flex:0 0 33%;flex:0 0 33%}.social_feed_container:not(.masonry).columns4 .social_feed_element{-ms-flex:0 0 25%;flex:0 0 25%}.social_feed_container.masonry.columns2 .social_feed_element,.social_feed_container.masonry.columns3 .social_feed_element,.social_feed_container.masonry.columns4 .social_feed_element{float:left}.social_feed_container.masonry.columns2 .social_feed_element{width:50%}.social_feed_container.masonry.columns3 .social_feed_element{width:33.33%}.social_feed_container.masonry.columns4 .social_feed_element{width:25%}@media (max-width:767px){.social_feed_container{width:100%;margin-left:0}.social_feed_container:not(.masonry).columns2 .social_feed_element,.social_feed_container:not(.masonry).columns3 .social_feed_element,.social_feed_container:not(.masonry).columns4 .social_feed_element{-ms-flex:0 0 100%;flex:0 0 100%}.social_feed_container.masonry.columns2 .social_feed_element,.social_feed_container.masonry.columns3 .social_feed_element,.social_feed_container.masonry.columns4 .social_feed_element{float:none;width:100%}} -------------------------------------------------------------------------------- /src/Command/TwitterImportCommand.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Command; 22 | 23 | use Contao\CoreBundle\Framework\ContaoFramework; 24 | use Pdir\SocialFeedBundle\Cron\TwitterImportCron; 25 | use Symfony\Component\Console\Attribute\AsCommand; 26 | use Symfony\Component\Console\Command\Command; 27 | use Symfony\Component\Console\Exception\InvalidArgumentException; 28 | use Symfony\Component\Console\Input\InputArgument; 29 | use Symfony\Component\Console\Input\InputInterface; 30 | use Symfony\Component\Console\Input\InputOption; 31 | use Symfony\Component\Console\Output\OutputInterface; 32 | 33 | /** 34 | * @internal 35 | */ 36 | #[AsCommand( 37 | name: 'social-feed:x:import', 38 | description: 'Import Twitter posts from API.', 39 | aliases: ['sf:x'] 40 | )] 41 | /** 42 | * Import Twitter posts. 43 | */ 44 | class TwitterImportCommand extends Command 45 | { 46 | public function __construct(private ContaoFramework $framework) 47 | { 48 | parent::__construct(); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | protected function configure(): void 55 | { 56 | $this->addOption('max-posts', 'm', InputOption::VALUE_REQUIRED, 'The maximum number of posts to execute (default 100)', '100'); 57 | $this->addOption('enable-debug', 'd', InputArgument::OPTIONAL, "Log debug information to console"); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | protected function execute(InputInterface $input, OutputInterface $output): int 64 | { 65 | $this->framework->initialize(); 66 | 67 | $output->writeln('Social Feed: Run Twitter import ...'); 68 | 69 | try { 70 | $cron = new TwitterImportCron($this->framework); 71 | $cron->setPoorManCronMode(false); 72 | $cron(); 73 | } catch (InvalidArgumentException $e) { 74 | $output->writeln(sprintf('%s (see help social-feed:x:import).', $e->getMessage())); 75 | 76 | return Command::FAILURE; 77 | } 78 | 79 | if(0 < $cron->counter) { 80 | $output->writeln('... imported ' . $cron->counter . ' items.'); 81 | } 82 | 83 | if (0 === $cron->counter) { 84 | $output->writeln('... nothing to import'); 85 | } 86 | 87 | return Command::SUCCESS; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Command/FacebookImportCommand.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Command; 22 | 23 | use Contao\CoreBundle\Framework\ContaoFramework; 24 | use Pdir\SocialFeedBundle\Cron\FacebookImportCron; 25 | use Symfony\Component\Console\Attribute\AsCommand; 26 | use Symfony\Component\Console\Command\Command; 27 | use Symfony\Component\Console\Exception\InvalidArgumentException; 28 | use Symfony\Component\Console\Input\InputArgument; 29 | use Symfony\Component\Console\Input\InputInterface; 30 | use Symfony\Component\Console\Input\InputOption; 31 | use Symfony\Component\Console\Output\OutputInterface; 32 | 33 | /** 34 | * @internal 35 | */ 36 | #[AsCommand( 37 | name: 'social-feed:facebook:import', 38 | description: 'Import Facebook posts from API.', 39 | aliases: ['sf:facebook'] 40 | )] 41 | /** 42 | * Import Facebook posts. 43 | */ 44 | class FacebookImportCommand extends Command 45 | { 46 | public function __construct(private ContaoFramework $framework) 47 | { 48 | parent::__construct(); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | protected function configure(): void 55 | { 56 | $this->addOption('max-posts', 'm', InputOption::VALUE_REQUIRED, 'The maximum number of posts to execute (default 100)', '100'); 57 | $this->addOption('enable-debug', 'd', InputArgument::OPTIONAL, 'Log debug information to console'); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | protected function execute(InputInterface $input, OutputInterface $output): int 64 | { 65 | $this->framework->initialize(); 66 | 67 | $output->writeln('Social Feed: Run Facebook import ...'); 68 | 69 | try { 70 | $cron = new FacebookImportCron($this->framework); 71 | $cron->setPoorManCronMode(false); 72 | $cron(); 73 | } catch (InvalidArgumentException $e) { 74 | $output->writeln(sprintf('%s (see help social-feed:facebook:import).', $e->getMessage())); 75 | 76 | return Command::FAILURE; 77 | } 78 | 79 | if (0 < $cron->counter) { 80 | $output->writeln('imported '.$cron->counter.' items.'); 81 | } 82 | 83 | if (0 === $cron->counter) { 84 | $output->writeln('... nothing to import'); 85 | } 86 | 87 | return Command::SUCCESS; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Command/InstagramImportCommand.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Command; 22 | 23 | use Contao\CoreBundle\Framework\ContaoFramework; 24 | use Pdir\SocialFeedBundle\Cron\InstagramImportCron; 25 | use Symfony\Component\Console\Attribute\AsCommand; 26 | use Symfony\Component\Console\Command\Command; 27 | use Symfony\Component\Console\Exception\InvalidArgumentException; 28 | use Symfony\Component\Console\Input\InputArgument; 29 | use Symfony\Component\Console\Input\InputInterface; 30 | use Symfony\Component\Console\Input\InputOption; 31 | use Symfony\Component\Console\Output\OutputInterface; 32 | 33 | /** 34 | * @internal 35 | */ 36 | #[AsCommand( 37 | name: 'social-feed:instagram:import', 38 | description: 'Import Instagram posts from API.', 39 | aliases: ['sf:instagram'] 40 | )] 41 | /** 42 | * Import Instagram posts. 43 | */ 44 | class InstagramImportCommand extends Command 45 | { 46 | public function __construct(private ContaoFramework $framework) 47 | { 48 | parent::__construct(); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | protected function configure(): void 55 | { 56 | $this->addOption('max-posts', 'm', InputOption::VALUE_REQUIRED, 'The maximum number of posts to execute (default 100)', '100'); 57 | $this->addOption('enable-debug', 'd', InputArgument::OPTIONAL, "Log debug information to console"); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | protected function execute(InputInterface $input, OutputInterface $output): int 64 | { 65 | $this->framework->initialize(); 66 | 67 | $output->writeln('Social Feed: Run Instagram import ...'); 68 | 69 | try { 70 | $cron = new InstagramImportCron($this->framework); 71 | $cron->setPoorManCronMode(false); 72 | $cron(); 73 | } catch (InvalidArgumentException $e) { 74 | $output->writeln(sprintf('%s (see help social-feed:instagram:import).', $e->getMessage())); 75 | 76 | return Command::FAILURE; 77 | } 78 | 79 | if(0 < $cron->counter) { 80 | $output->writeln('... imported ' . $cron->counter . ' items.'); 81 | } 82 | 83 | if (0 === $cron->counter) { 84 | $output->writeln('... nothing to import'); 85 | } 86 | 87 | return Command::SUCCESS; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /contao/languages/it/default.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['importMessage'] = 'Gli elementi %s sono stati importati.'; 22 | $GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInSubject'] = 'LinkedIn Access Token Erinnerung'; 23 | $GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInHtml'] = 'Ciao Admin,

Il token di accesso LinkedIn sul sito %s per l\'account %s deve essere rigenerato. Per farlo, accedi al backend di Contao, apri le impostazioni dell\'account del social feed, seleziona la casella di controllo "Generate Access Token" e salva. In seguito, dovrai solo permettere l\'accesso all\'app e il Token di accesso sarà generato di nuovo. Questo processo deve essere ripetuto ogni anno. Alimentato da Social Feed+ Bundle'; 24 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['noItems'] = 'Non sono state trovate voci.'; 25 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['headline'] = 'Notizie › Alimentazione sociale › Moderato › Archivio '; 26 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['facebookNotSupported'] = 'I post di Facebook non possono ancora essere importati nell\'archivio delle notizie tramite l\'importazione manuale. Si prega di impostare un cronjob nell\'account del social feed e di utilizzare l\'importazione automatica. Attualmente, solo i post di Instagram possono essere importati manualmente.'; 27 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['twitterNotSupported'] = 'I post di X non possono ancora essere importati nell\'archivio delle notizie tramite l\'importazione manuale. Si prega di impostare un cronjob nell\'account del social feed e di utilizzare l\'importazione automatica. Attualmente, solo i post di Instagram possono essere importati manualmente.'; 28 | $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['linkedInNotSupported'] = 'I post di LinkedIn non possono ancora essere importati nell\'archivio delle notizie tramite l\'importazione manuale. Si prega di impostare un cronjob nell\'account del social feed e di utilizzare l\'importazione automatica. Attualmente, solo i post di Instagram possono essere importati manualmente.'; 29 | $GLOBALS['TL_LANG']['BE_MOD']['psfNoAccessToken'] = 'Nessun token di accesso dato.'; 30 | $GLOBALS['TL_LANG']['MSC']['pdirSocialFeedNoTitel'] = 'Nessun titolo'; 31 | $GLOBALS['TL_LANG']['MSC']['socialFeedAccounts'] = 'Social Feed Conti'; 32 | -------------------------------------------------------------------------------- /src/EventListener/DataContainer/SetupListener.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\EventListener\DataContainer; 22 | 23 | use Contao\BackendTemplate; 24 | use Contao\DataContainer; 25 | use Safe\Exceptions\StringsException; 26 | 27 | class SetupListener 28 | { 29 | /** 30 | * social-feed-bundle version. 31 | */ 32 | public const VERSION = '2.13.5'; 33 | 34 | /** 35 | * Template. 36 | */ 37 | protected string $strTemplate = 'be_socialfeed_setup'; 38 | 39 | /** 40 | * On generate the label. 41 | * 42 | * @throws StringsException 43 | */ 44 | public function onGenerateLabel(array $row): string 45 | { 46 | $account = ''; 47 | 48 | // set account type 49 | switch ($row['socialFeedType']) { 50 | case 'Facebook': 51 | $account = $row['pdir_sf_fb_account']; 52 | break; 53 | 54 | case 'Instagram': 55 | $account = $row['instagram_account']; 56 | break; 57 | 58 | case 'Twitter': 59 | $account = $row['twitter_account']; 60 | break; 61 | 62 | case 'LinkedIn': 63 | $account = $row['linkedin_company_id']; 64 | break; 65 | } 66 | 67 | // no account is selected 68 | if (empty($account)) { 69 | $account = $GLOBALS['TL_LANG']['tl_social_feed']['psfLabelNoAccount']; 70 | } 71 | 72 | if (!empty($row['search'])) { 73 | $row['search'] = sprintf($GLOBALS['TL_LANG']['tl_social_feed']['psfLabelSearchTerm'], $row['search']); 74 | } 75 | 76 | return sprintf( 77 | '%s → %s %s', 78 | $row['socialFeedType'] ?: $GLOBALS['TL_LANG']['tl_social_feed']['psfLabelNoType'], 79 | $account, 80 | $row['search'] 81 | ); 82 | } 83 | 84 | public function renderFooter(DataContainer $dc): string 85 | { 86 | // add setupExplanation 87 | return $this->setupExplanation($dc); 88 | } 89 | 90 | /** 91 | * Gets the setup explanation. 92 | */ 93 | public function setupExplanation(DataContainer $dc): string 94 | { 95 | $template = new BackendTemplate($this->strTemplate); 96 | $template->version = self::VERSION; 97 | 98 | return $template->parse(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autoconfigure: true 4 | autowire: true 5 | 6 | Pdir\SocialFeedBundle\: 7 | resource: ../src/* 8 | 9 | Pdir\SocialFeedBundle\EventListener\DataContainer\SetupListener: 10 | public: true 11 | 12 | Pdir\SocialFeedBundle\Importer\InstagramClient: 13 | public: true 14 | arguments: 15 | - "@Pdir\\SocialFeedBundle\\Importer\\InstagramRequestCache" 16 | - "@contao.framework" 17 | - "@?logger" 18 | tags: 19 | - { name: monolog.logger, channel: contao } 20 | 21 | Pdir\SocialFeedBundle\Importer\InstagramRequestCache: 22 | public: true 23 | arguments: 24 | - "@filesystem" 25 | - ~ # cache_ttl to be set in extension class 26 | - "%kernel.project_dir%" 27 | 28 | Pdir\SocialFeedBundle\Controller\InstagramController: 29 | public: true 30 | arguments: 31 | - "@Pdir\\SocialFeedBundle\\Importer\\InstagramClient" 32 | - "@database_connection" 33 | - "@router" 34 | - "@security.token_storage" 35 | 36 | Pdir\SocialFeedBundle\Controller\LinkedinController: 37 | public: true 38 | arguments: 39 | - "@database_connection" 40 | - "@router" 41 | 42 | Pdir\SocialFeedBundle\Controller\FacebookController: 43 | public: true 44 | arguments: 45 | - "@database_connection" 46 | - "@router" 47 | 48 | Pdir\SocialFeedBundle\EventListener\DataContainer\SocialFeedListener: 49 | public: true 50 | 51 | pdir_social_feed_moderate.controller: 52 | class: Pdir\SocialFeedBundle\Controller\ModerateController 53 | public: true 54 | arguments: 55 | - "@contao.framework" 56 | - "@request_stack" 57 | - '@contao.csrf.token_manager' 58 | 59 | Pdir\SocialFeedBundle\Cron\FacebookImportCron: 60 | public: true 61 | tags: 62 | - { name: contao.cron, cli: true, priority: 10 } 63 | arguments: 64 | - '@contao.framework' 65 | 66 | Pdir\SocialFeedBundle\Cron\InstagramImportCron: 67 | public: true 68 | tags: 69 | - { name: contao.cron, cli: true, priority: 10 } 70 | arguments: 71 | - '@contao.framework' 72 | 73 | Pdir\SocialFeedBundle\Cron\LinkedInImportCron: 74 | public: true 75 | tags: 76 | - { name: contao.cron, cli: true, priority: 10 } 77 | arguments: 78 | - '@contao.framework' 79 | - 80 | Pdir\SocialFeedBundle\Cron\RefreshAccessTokenCron: 81 | public: true 82 | tags: 83 | - { name: contao.cron, cli: true, priority: 10 } 84 | arguments: 85 | - '@contao.framework' 86 | 87 | Pdir\SocialFeedBundle\Cron\TwitterImportCron: 88 | public: true 89 | tags: 90 | - { name: contao.cron, cli: true, priority: 10 } 91 | arguments: 92 | - '@contao.framework' 93 | 94 | Pdir\SocialFeedBundle\SocialFeed\SocialFeedNewsClass: 95 | public: true 96 | -------------------------------------------------------------------------------- /src/Command/LinkedInImportCommand.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Command; 22 | 23 | use Contao\CoreBundle\Framework\ContaoFramework; 24 | use Pdir\SocialFeedBundle\Importer\LinkedIn; 25 | use Symfony\Component\Console\Attribute\AsCommand; 26 | use Symfony\Component\Console\Command\Command; 27 | use Symfony\Component\Console\Exception\InvalidArgumentException; 28 | use Symfony\Component\Console\Input\InputArgument; 29 | use Symfony\Component\Console\Input\InputInterface; 30 | use Symfony\Component\Console\Input\InputOption; 31 | use Symfony\Component\Console\Output\OutputInterface; 32 | 33 | /** 34 | * @internal 35 | */ 36 | #[AsCommand( 37 | name: 'social-feed:linkedin:import', 38 | description: 'Import LinkedIn posts from API.', 39 | aliases: ['sf:linkedin'] 40 | )] 41 | /** 42 | * Import LinkedIn posts. 43 | */ 44 | class LinkedInImportCommand extends Command 45 | { 46 | public function __construct(private ContaoFramework $framework) 47 | { 48 | parent::__construct(); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | protected function configure(): void 55 | { 56 | $this->addOption('max-posts', 'm', InputOption::VALUE_REQUIRED, 'The maximum number of posts to execute (default 100)', '100'); 57 | $this->addOption('enable-debug', 'd', InputArgument::OPTIONAL, "Log debug information to console"); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | protected function execute(InputInterface $input, OutputInterface $output): int 64 | { 65 | $this->framework->initialize(); 66 | 67 | $output->writeln('Social Feed: Run LinkedIn import ...'); 68 | 69 | try { 70 | $importer = new LinkedIn(); 71 | $importer->setIgnoreInterval(true); 72 | $importer->setDebugMode((bool) $input->getOption('enable-debug')); 73 | $importer->setMaxPosts((int) $input->getOption('max-posts')?? 100); 74 | $strLog = $importer->import(); 75 | 76 | } catch (InvalidArgumentException $e) { 77 | $output->writeln(sprintf('%s (see help social-feed:linkedin:import).', $e->getMessage())); 78 | 79 | return Command::FAILURE; 80 | } 81 | 82 | if(0 < $importer->counter) { 83 | $output->writeln('... imported ' . $importer->counter . ' items.'); 84 | } 85 | 86 | if (0 === $importer->counter) { 87 | $output->writeln('... nothing to import'); 88 | } 89 | 90 | return Command::SUCCESS; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/css/sf_moderation.scss: -------------------------------------------------------------------------------- 1 | /** moderation **/ 2 | 3 | .mod_sf_moderate { 4 | h1 { 5 | width: 80%; 6 | } 7 | h2.sub_headline { 8 | margin: 3px 15px; 9 | } 10 | #tl_buttons { 11 | float: right; 12 | } 13 | } 14 | 15 | .be_sf_morderation_list { 16 | .list { 17 | display: flex; 18 | flex-wrap: wrap; 19 | list-style: none; 20 | padding: 0 5px; 21 | } 22 | .list-item { 23 | box-sizing: border-box; 24 | flex: 0 0 auto; 25 | padding: 1em; 26 | margin: 0.5em; 27 | width: calc(33.3% - 1em); 28 | border: 1px solid #ccc; 29 | position: relative; 30 | 31 | .list-content { 32 | background-color: #fff; 33 | width: 100%; 34 | } 35 | 36 | .list-content p { 37 | margin-top: 10px; 38 | } 39 | 40 | .checkbox-container { 41 | display: block; 42 | position: absolute; 43 | right: 0; 44 | top: 10px; 45 | padding-left: 35px; 46 | margin-bottom: 12px; 47 | cursor: pointer; 48 | font-size: 22px; 49 | -webkit-user-select: none; 50 | -moz-user-select: none; 51 | -ms-user-select: none; 52 | user-select: none; 53 | 54 | input { 55 | position: absolute; 56 | opacity: 0; 57 | cursor: pointer; 58 | height: 0; 59 | width: 0; 60 | } 61 | 62 | .checkmark { 63 | position: absolute; 64 | top: 0; 65 | left: 0; 66 | height: 25px; 67 | width: 25px; 68 | background-color: #eee; 69 | 70 | &:after { 71 | content: ""; 72 | position: absolute; 73 | display: none; 74 | left: 9px; 75 | top: 5px; 76 | width: 5px; 77 | height: 10px; 78 | border: solid white; 79 | border-width: 0 3px 3px 0; 80 | -webkit-transform: rotate(45deg); 81 | -ms-transform: rotate(45deg); 82 | transform: rotate(45deg); 83 | } 84 | } 85 | 86 | &:hover input ~ .checkmark { 87 | background-color: #ccc; 88 | } 89 | 90 | input:checked ~ .checkmark { 91 | background-color: #2196F3; 92 | 93 | &:after { 94 | display: block; 95 | } 96 | 97 | } 98 | } 99 | } 100 | 101 | .tl_formbody_submit { 102 | position: relative; 103 | 104 | .tl_submit_container { 105 | position: fixed; 106 | bottom: 0; 107 | width: 100%; 108 | } 109 | } 110 | } 111 | 112 | a.header_sf_moderate { 113 | background-size: 16px; 114 | } 115 | 116 | @media all and (min-width: 40em) { 117 | .be_sf_morderation_list .list-item { 118 | wi2dth: 50%; 119 | } 120 | } 121 | @media all and (min-width: 60em) { 122 | .be_sf_morderation_list .list-item { 123 | wi2dth: 33.33%; 124 | } 125 | } 126 | 127 | html[data-color-scheme="dark"] { 128 | .be_sf_morderation_list .list-item { 129 | border: 1px solid var(--form-border); 130 | .list-content { 131 | background: var(--content-bg); 132 | } 133 | 134 | .checkbox-container { 135 | .checkmark { 136 | border: solid var(--form-border); 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /public/css/sf_moderation.css: -------------------------------------------------------------------------------- 1 | /** moderation **/ 2 | .mod_sf_moderate h1 { 3 | width: 80%; 4 | } 5 | .mod_sf_moderate h2.sub_headline { 6 | margin: 3px 15px; 7 | } 8 | .mod_sf_moderate #tl_buttons { 9 | float: right; 10 | } 11 | 12 | .be_sf_morderation_list .list { 13 | display: flex; 14 | flex-wrap: wrap; 15 | list-style: none; 16 | padding: 0 5px; 17 | } 18 | .be_sf_morderation_list .list-item { 19 | box-sizing: border-box; 20 | flex: 0 0 auto; 21 | padding: 1em; 22 | margin: 0.5em; 23 | width: calc(33.3% - 1em); 24 | border: 1px solid #ccc; 25 | position: relative; 26 | } 27 | .be_sf_morderation_list .list-item .list-content { 28 | background-color: #fff; 29 | width: 100%; 30 | } 31 | .be_sf_morderation_list .list-item .list-content p { 32 | margin-top: 10px; 33 | } 34 | .be_sf_morderation_list .list-item .checkbox-container { 35 | display: block; 36 | position: absolute; 37 | right: 0; 38 | top: 10px; 39 | padding-left: 35px; 40 | margin-bottom: 12px; 41 | cursor: pointer; 42 | font-size: 22px; 43 | -webkit-user-select: none; 44 | -moz-user-select: none; 45 | -ms-user-select: none; 46 | user-select: none; 47 | } 48 | .be_sf_morderation_list .list-item .checkbox-container input { 49 | position: absolute; 50 | opacity: 0; 51 | cursor: pointer; 52 | height: 0; 53 | width: 0; 54 | } 55 | .be_sf_morderation_list .list-item .checkbox-container .checkmark { 56 | position: absolute; 57 | top: 0; 58 | left: 0; 59 | height: 25px; 60 | width: 25px; 61 | background-color: #eee; 62 | } 63 | .be_sf_morderation_list .list-item .checkbox-container .checkmark:after { 64 | content: ""; 65 | position: absolute; 66 | display: none; 67 | left: 9px; 68 | top: 5px; 69 | width: 5px; 70 | height: 10px; 71 | border: solid white; 72 | border-width: 0 3px 3px 0; 73 | -webkit-transform: rotate(45deg); 74 | -ms-transform: rotate(45deg); 75 | transform: rotate(45deg); 76 | } 77 | .be_sf_morderation_list .list-item .checkbox-container:hover input ~ .checkmark { 78 | background-color: #ccc; 79 | } 80 | .be_sf_morderation_list .list-item .checkbox-container input:checked ~ .checkmark { 81 | background-color: #2196F3; 82 | } 83 | .be_sf_morderation_list .list-item .checkbox-container input:checked ~ .checkmark:after { 84 | display: block; 85 | } 86 | .be_sf_morderation_list .tl_formbody_submit { 87 | position: relative; 88 | } 89 | .be_sf_morderation_list .tl_formbody_submit .tl_submit_container { 90 | position: fixed; 91 | bottom: 0; 92 | width: 100%; 93 | } 94 | 95 | a.header_sf_moderate { 96 | background-size: 16px; 97 | } 98 | 99 | @media all and (min-width: 40em) { 100 | .be_sf_morderation_list .list-item { 101 | wi2dth: 50%; 102 | } 103 | } 104 | @media all and (min-width: 60em) { 105 | .be_sf_morderation_list .list-item { 106 | wi2dth: 33.33%; 107 | } 108 | } 109 | html[data-color-scheme=dark] .be_sf_morderation_list .list-item { 110 | border: 1px solid var(--form-border); 111 | } 112 | html[data-color-scheme=dark] .be_sf_morderation_list .list-item .list-content { 113 | background: var(--content-bg); 114 | } 115 | html[data-color-scheme=dark] .be_sf_morderation_list .list-item .checkbox-container .checkmark { 116 | border: solid var(--form-border); 117 | } 118 | 119 | /*# sourceMappingURL=sf_moderation.css.map */ 120 | -------------------------------------------------------------------------------- /src/Controller/LinkedinController.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Controller; 22 | 23 | use Contao\CoreBundle\Monolog\ContaoContext; 24 | use Contao\System; 25 | use Doctrine\DBAL\Connection; 26 | use Pdir\SocialFeedBundle\EventListener\DataContainer\SocialFeedListener; 27 | use Symfony\Component\HttpFoundation\RedirectResponse; 28 | use Symfony\Component\HttpFoundation\Request; 29 | use Symfony\Component\HttpFoundation\Response; 30 | use Symfony\Component\HttpFoundation\Session\SessionInterface; 31 | use Symfony\Component\Routing\Annotation\Route; 32 | use Symfony\Component\Routing\RouterInterface; 33 | 34 | #[Route('auth', defaults: ['_scope' => 'backend', '_token_check' => false])] 35 | class LinkedinController 36 | { 37 | private Connection $db; 38 | 39 | private RouterInterface $router; 40 | 41 | private SessionInterface $session; 42 | 43 | /** 44 | * LinkedinController constructor. 45 | */ 46 | public function __construct(Connection $db, RouterInterface $router) 47 | { 48 | $this->db = $db; 49 | $this->router = $router; 50 | $this->session = System::getContainer()->get('request_stack')->getCurrentRequest()->getSession(); 51 | } 52 | 53 | #[Route('/linkedin', name: 'auth_linkedin', methods: ['GET'])] 54 | public function authAction(Request $request): Response 55 | { 56 | $sessionData = $this->session->get(SocialFeedListener::SESSION_KEY); 57 | 58 | // Missing code query parameter 59 | if (!$request->query->get('code')) { 60 | return new Response(Response::$statusTexts[Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); 61 | } 62 | //get refresh token 63 | $data = [ 64 | 'grant_type' => 'authorization_code', 65 | 'code' => $request->query->get('code'), 66 | 'client_id' => $sessionData['clientId'], 67 | 'client_secret' => \str_replace('=', '=', $sessionData['clientSecret']), 68 | 'redirect_uri' => $this->router->generate('auth_linkedin', [], RouterInterface::ABSOLUTE_URL), 69 | ]; 70 | 71 | try { 72 | $token = json_decode(file_get_contents('https://www.linkedin.com/oauth/v2/accessToken?'.http_build_query($data))); 73 | 74 | // Store the access token and remove temporary session key 75 | $this->db->update('tl_social_feed', ['linkedin_access_token' => $token->access_token, 'access_token_expires' => time() + $token->expires_in, 'linkedin_refresh_token' => $token->refresh_token, 'linkedin_refresh_token_expires' => time() + $token->refresh_token_expires_in], ['id' => $sessionData['socialFeedId']]); 76 | $this->session->remove(SocialFeedListener::SESSION_KEY); 77 | } catch (\Exception $e) { 78 | System::log($e->getMessage(), __METHOD__, ContaoContext::GENERAL); 79 | } 80 | 81 | return new RedirectResponse($sessionData['backUrl']); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/css/backend.css: -------------------------------------------------------------------------------- 1 | 2 | #tl_navigation .group-pdir { 3 | background: url(/bundles/pdirsocialfeed/img/pdir_logo.svg) 0 0 no-repeat; 4 | background-size: 18px 18px; 5 | } 6 | 7 | .be_socialfeed_setup a { 8 | color: #7abfbc; 9 | } 10 | 11 | .be_socialfeed_setup .right { 12 | float: right; 13 | width: 40%; 14 | margin: 15px 0; 15 | } 16 | .be_socialfeed_setup .left { 17 | float: left; 18 | width: 50%; 19 | margin: 15px 0; 20 | } 21 | 22 | .be_socialfeed_setup .logo { 23 | margin-bottom: 5px; 24 | } 25 | 26 | .be_socialfeed_setup h2 { 27 | margin-bottom: 10px; 28 | } 29 | 30 | .be_socialfeed_setup hr { 31 | width: 100%; 32 | margin: 20px auto; 33 | } 34 | 35 | .be_socialfeed_setup .devlog { 36 | margin: 10px 0; 37 | } 38 | 39 | .be_socialfeed_setup .link-list li { 40 | padding: 5px 0 5px 0; 41 | } 42 | 43 | .be_socialfeed_setup .feed .item { 44 | margin-bottom: 5px; 45 | } 46 | 47 | .be_socialfeed_setup .feed .item span { 48 | padding-right: 10px; 49 | color: #7abfbc; 50 | } 51 | 52 | .be_socialfeed_setup .button { 53 | float: left; 54 | text-align: center; 55 | margin-right: 25px; 56 | } 57 | 58 | .be_socialfeed_setup .kreis { 59 | width: 60px; 60 | padding: 13px; 61 | box-sizing: border-box; 62 | margin:auto; 63 | } 64 | 65 | .be_socialfeed_setup small { 66 | color: #cccccc; 67 | } 68 | 69 | .be_socialfeed_setup .api-status .blink { 70 | width: 15px; 71 | height: 15px; 72 | padding: 10px; 73 | box-sizing: border-box; 74 | border-radius: 10px; 75 | background: #ACDA3D; 76 | display: inline-block; 77 | position: relative; 78 | top: 5px; 79 | margin-left: 5px; 80 | } 81 | 82 | .be_socialfeed_setup .api-status .blink.red { 83 | background: #cc3d09; 84 | } 85 | 86 | .be_socialfeed_setup .debug-info { 87 | background: #ccc; 88 | color: #fff; 89 | padding: 10px; 90 | margin-top: 15px; 91 | } 92 | 93 | .be_socialfeed_setup .plus-logo { 94 | float: right; 95 | } 96 | 97 | .be_socialfeed_setup .high-plus { 98 | content: ''; 99 | background: url(/bundles/pdirsocialfeed/img/icon_plus.svg) 0 0 no-repeat; 100 | background-size: 12px; 101 | display: inline-block; 102 | width: 12px; 103 | height: 12px; 104 | margin-left: 5px; 105 | } 106 | 107 | .be_socialfeed_setup .benefit { 108 | margin-top: 12px; 109 | line-height: 18px; 110 | list-style: none; 111 | } 112 | 113 | .be_socialfeed_setup .benefit li { 114 | margin-left: 20px; 115 | } 116 | 117 | .be_socialfeed_setup .benefit li:before { 118 | content: ''; 119 | background: url(/bundles/pdirsocialfeed/img/icon_plus.svg) 0 0 no-repeat; 120 | background-size: 12px; 121 | height: 12px; 122 | width: 12px; 123 | display: inline-block; 124 | position: absolute; 125 | margin-left: -19px; 126 | margin-top: 3px; 127 | } 128 | 129 | .selectAll { 130 | margin: 15px 15px -5px; 131 | display: inline-block; 132 | } 133 | 134 | .selectAll input { 135 | margin-right: 5px; 136 | } 137 | 138 | a.header_socialFeedAccounts { 139 | background-size: 16px; 140 | } 141 | 142 | .header_sf_moderate { 143 | margin-left: 34px; 144 | } 145 | 146 | .header_sf_moderate:before { 147 | content: ""; 148 | position: absolute; 149 | width: 16px; 150 | height: 16px; 151 | margin-left: -22px; 152 | background: url(/bundles/pdirsocialfeed/img/icon_fa_download-solid.svg) 0 0 no-repeat; 153 | } 154 | 155 | html[data-color-scheme="dark"] { 156 | .header_socialFeedAccounts, 157 | .header_sf_moderate:before { 158 | filter: invert(1); 159 | } 160 | 161 | .header_socialFeedAccounts { 162 | color: #000; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: ~ 5 | push: ~ 6 | 7 | jobs: 8 | ecs: 9 | name: ECS 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Setup PHP 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: 7.4 # Run with the lowest supported to avoid incompatible fixes 16 | extensions: dom, fileinfo, filter, gd, hash, intl, json, mbstring, mysqli, pcre, pdo_mysql, zlib 17 | coverage: none 18 | env: 19 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Install the dependencies 25 | run: composer install --no-interaction --no-progress 26 | 27 | - name: Run ECS 28 | run: vendor/bin/ecs check src tests --config ecs.php --no-progress-bar --ansi 29 | 30 | phpstan: 31 | name: PHPStan 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: 7.4 38 | extensions: dom, fileinfo, filter, gd, hash, intl, json, mbstring, mysqli, pcre, pdo_mysql, zlib 39 | coverage: none 40 | env: 41 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Checkout 44 | uses: actions/checkout@v3 45 | 46 | - name: Install the dependencies 47 | run: | 48 | composer install --no-interaction --no-progress 49 | 50 | - name: Run PHPStan 51 | run: vendor/bin/phpstan analyse src tests --no-progress 52 | 53 | # tests: 54 | # name: PHP ${{ matrix.php }} 55 | # runs-on: ubuntu-latest 56 | # strategy: 57 | # fail-fast: false 58 | # matrix: 59 | # php: [ 7.4, 8.0, 8.1 ] 60 | # steps: 61 | # - name: Setup PHP 62 | # uses: shivammathur/setup-php@v2 63 | # with: 64 | # php-version: ${{ matrix.php }} 65 | # extensions: dom, fileinfo, filter, gd, hash, intl, json, mbstring, mysqli, pcre, pdo_mysql, zlib 66 | # coverage: none 67 | # env: 68 | # COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | # 70 | # - name: Checkout 71 | # uses: actions/checkout@v3 72 | # 73 | # - name: Install the dependencies 74 | # run: composer install --no-interaction --no-progress 75 | # 76 | # - name: Run the unit tests 77 | # run: vendor/bin/phpunit --colors=always 78 | 79 | # nightly: 80 | # name: PHP 8.2 81 | # runs-on: ubuntu-latest 82 | # continue-on-error: true 83 | # steps: 84 | # - name: Setup PHP 85 | # uses: shivammathur/setup-php@v2 86 | # with: 87 | # php-version: 8.2 88 | # extensions: dom, fileinfo, filter, gd, hash, intl, json, mbstring, mysqli, pcre, pdo_mysql, zlib 89 | # coverage: none 90 | # env: 91 | # COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | # 93 | # - name: Checkout 94 | # uses: actions/checkout@v3 95 | # 96 | # - name: Install the dependencies 97 | # run: composer install --ignore-platform-req=php --no-interaction --no-progress 98 | # 99 | # - name: Run the unit tests 100 | # run: vendor/bin/phpunit --colors=always 101 | -------------------------------------------------------------------------------- /public/css/social_feed.scss: -------------------------------------------------------------------------------- 1 | .social_feed_element { 2 | overflow: visible; 3 | padding: 0; 4 | width: 100%; 5 | 6 | &.extended { 7 | .inner { 8 | a { 9 | display: inline; 10 | padding: 0; 11 | color: rgb(27, 149, 224); 12 | 13 | &:hover { 14 | text-decoration: underline; 15 | } 16 | } 17 | 18 | > figure { 19 | padding: 0; 20 | } 21 | } 22 | 23 | a.more { 24 | padding: 0; 25 | font-size: 14px; 26 | color: rgb(27, 149, 224); 27 | font-weight: 700; 28 | display: block; 29 | 30 | &:hover { 31 | text-decoration: underline; 32 | } 33 | } 34 | 35 | .inner { 36 | padding: 0 20px 20px; 37 | } 38 | 39 | p { 40 | &:first-child { 41 | margin-top: 0; 42 | } 43 | &:last-child { 44 | margin-bottom: 0; 45 | } 46 | } 47 | } 48 | 49 | .inner { 50 | background: #f2f2f2; 51 | padding: 0; 52 | margin: 30px 10px 10px; 53 | position: relative; 54 | word-break: break-word; 55 | border-top: 5px solid #c1c1c1; 56 | 57 | a { 58 | padding: 0 20px; 59 | display: block; 60 | } 61 | 62 | > figure { 63 | padding: 0 20px; 64 | } 65 | } 66 | 67 | .ce_text { 68 | padding: 15px 0; 69 | color: #333; 70 | margin: 0; 71 | } 72 | 73 | .icon { 74 | position: static; 75 | text-indent: 0; 76 | 77 | img { 78 | position: absolute; 79 | top: -20px; 80 | border-radius: 100%; 81 | max-width: 50px; 82 | } 83 | 84 | .image-wrapper:not(.loaded) { 85 | height: 0 !important; 86 | } 87 | } 88 | 89 | .info { 90 | margin: 0; 91 | padding: 10px 0 0; 92 | text-align: right; 93 | font-size: 14px; 94 | color: #989898; 95 | display: flex; 96 | align-items: center; 97 | justify-content: flex-end; 98 | 99 | img { 100 | width: 15px; 101 | margin-left: 10px; 102 | } 103 | } 104 | 105 | .title { 106 | margin: 10px 0; 107 | font-size: 16px; 108 | font-weight: 700; 109 | color: #333; 110 | } 111 | 112 | a { 113 | color: #333; 114 | text-decoration: none; 115 | } 116 | 117 | .ce_text { 118 | font-size: 14px; 119 | } 120 | 121 | .fa { 122 | font-size: 18px; 123 | padding-left: 5px; 124 | } 125 | 126 | .image_container > a { 127 | padding: 0; 128 | } 129 | } 130 | 131 | .social_feed_container { 132 | width: calc(100% + 30px); 133 | margin-left: -15px; 134 | font-family: sans-serif; 135 | 136 | &:not(.masonry) { 137 | display: -ms-flexbox; 138 | display: flex; 139 | -ms-flex-wrap: wrap; 140 | flex-wrap: wrap; 141 | 142 | &.columns2 .social_feed_element { 143 | -ms-flex: 0 0 50%; 144 | flex: 0 0 50%; 145 | } 146 | 147 | &.columns3 .social_feed_element { 148 | -ms-flex: 0 0 33%; 149 | flex: 0 0 33%; 150 | } 151 | 152 | &.columns4 .social_feed_element { 153 | -ms-flex: 0 0 25%; 154 | flex: 0 0 25%; 155 | } 156 | } 157 | &.masonry { 158 | &.columns2, &.columns3, &.columns4 { 159 | .social_feed_element { 160 | float: left; 161 | } 162 | } 163 | 164 | &.columns2 .social_feed_element { 165 | width: 50%; 166 | } 167 | 168 | &.columns3 .social_feed_element { 169 | width: 33.33%; 170 | } 171 | 172 | &.columns4 .social_feed_element { 173 | width: 25%; 174 | } 175 | } 176 | } 177 | 178 | @media (max-width:767px) { 179 | .social_feed_container { 180 | width: 100%; 181 | margin-left: 0; 182 | 183 | &:not(.masonry) { 184 | &.columns2, &.columns3, &.columns4 { 185 | .social_feed_element { 186 | -ms-flex: 0 0 100%; 187 | flex: 0 0 100%; 188 | } 189 | } 190 | } 191 | 192 | &.masonry { 193 | &.columns2, &.columns3, &.columns4 { 194 | .social_feed_element { 195 | float: none; 196 | width: 100%; 197 | } 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Controller/FacebookController.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Controller; 22 | 23 | use Contao\Input; 24 | use Contao\Message; 25 | use Contao\System; 26 | use Doctrine\DBAL\Connection; 27 | use Pdir\SocialFeedBundle\EventListener\DataContainer\SocialFeedListener; 28 | use Symfony\Component\HttpFoundation\RedirectResponse; 29 | use Symfony\Component\HttpFoundation\Request; 30 | use Symfony\Component\HttpFoundation\Response; 31 | use Symfony\Component\HttpFoundation\Session\SessionInterface; 32 | use Symfony\Component\Routing\Annotation\Route; 33 | use Symfony\Component\Routing\RouterInterface; 34 | 35 | #[Route('_facebook', defaults: ['_scope' => 'backend', '_token_check' => false])] 36 | class FacebookController 37 | { 38 | private Connection $db; 39 | 40 | private RouterInterface $router; 41 | 42 | private SessionInterface $session; 43 | 44 | /** 45 | * FacebookController constructor. 46 | */ 47 | public function __construct(Connection $db, RouterInterface $router) 48 | { 49 | $this->db = $db; 50 | $this->router = $router; 51 | $this->session = System::getContainer()->get('request_stack')->getCurrentRequest()->getSession(); 52 | } 53 | 54 | #[Route('/auth', name: 'facebook_auth', methods: ['GET'])] 55 | public function authAction(Request $request): Response 56 | { 57 | $sessionData = $this->session->get(SocialFeedListener::SESSION_KEY); 58 | 59 | if (empty($sessionData['backUrl'])) { 60 | return new Response(Response::$statusTexts[Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); 61 | } 62 | 63 | $data = [ 64 | 'client_id' => $sessionData['clientId'], 65 | 'client_secret' => $sessionData['clientSecret'], 66 | 'redirect_uri' => $this->router->generate('facebook_auth', [], RouterInterface::ABSOLUTE_URL), 67 | 'code' => $request->query->get('code'), 68 | ]; 69 | 70 | $json = @file_get_contents('https://graph.facebook.com/v11.0/oauth/access_token?'.http_build_query($data)); 71 | 72 | if (false === $json) { 73 | Message::addError(Input::get('error_message')); 74 | 75 | return new RedirectResponse($sessionData['backUrl']); 76 | } 77 | 78 | $obj = json_decode($json); 79 | $userAccessToken = $obj->access_token; 80 | 81 | $json = @file_get_contents('https://graph.facebook.com/'.$sessionData['page'].'?fields=access_token&access_token='.$userAccessToken); 82 | 83 | // set error message 84 | if (\is_bool($json) && false === $json) { 85 | if (Input::get('error_message')) { 86 | Message::addError(Input::get('error_message')); 87 | } else { 88 | $pageAccessToken = 'FACEBOOK GRAPH ERROR: empty return! Please check your credentials or account name.'; 89 | Message::addError($pageAccessToken); 90 | } 91 | } 92 | 93 | if (!\is_bool($json)) { 94 | $obj = json_decode($json); 95 | $pageAccessToken = $obj->access_token; 96 | } 97 | 98 | // Store the access token and remove temporary session key 99 | $this->db->update('tl_social_feed', ['pdir_sf_fb_access_token' => $pageAccessToken], ['id' => $sessionData['socialFeedId']]); 100 | $this->session->remove(SocialFeedListener::SESSION_KEY); 101 | 102 | return new RedirectResponse($sessionData['backUrl']); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdir/social-feed-bundle", 3 | "description": "Social feed extension for Contao CMS", 4 | "keywords": [ 5 | "contao", 6 | "social", 7 | "feed", 8 | "facebook", 9 | "instagram", 10 | "x", 11 | "twitter", 12 | "google plus", 13 | "pinterest", 14 | "vk", 15 | "rss", 16 | "bundle", 17 | "linkedin" 18 | ], 19 | "type": "contao-bundle", 20 | "homepage": "https://pdir.de", 21 | "license": "LGPL-3.0-or-later", 22 | "authors": [ 23 | { 24 | "name": "Philipp Seibt", 25 | "homepage": "https://pdir.de/", 26 | "role": "Developer" 27 | }, 28 | { 29 | "name": "Mathias Arzberger", 30 | "homepage": "https://pdir.de/", 31 | "role": "Developer" 32 | }, 33 | { 34 | "name": "pdir GmbH", 35 | "homepage": "https://pdir.de/", 36 | "role": "Developer" 37 | } 38 | ], 39 | "support": { 40 | "issues": "https://github.com/pdir/social-feed-bundle/issues", 41 | "source": "https://github.com/pdir/social-feed-bundle", 42 | "docs": "https://docs.pdir.de", 43 | "donate": "https://contao-themes.net/sponsoring.html" 44 | }, 45 | "require": { 46 | "php": "^8.1", 47 | "ext-json": "*", 48 | "contao/core-bundle": "^5.3", 49 | "contao/news-bundle": "^5.3", 50 | "nickdnk/graph-sdk": "^7.0", 51 | "abraham/twitteroauth": "~4.0", 52 | "guzzlehttp/guzzle": "6.0 | ~7.7", 53 | "kevinrob/guzzle-cache-middleware": "^5.1", 54 | "doctrine/cache": "^2.1", 55 | "doctrine/dbal": "^2.0 || ^3.0", 56 | "samoritano/linkedin-api-php-client-v2": "^0.1" 57 | }, 58 | "conflict": { 59 | "contao/core": "*", 60 | "contao/manager-plugin": "<2.0 || >=3.0" 61 | }, 62 | "require-dev": { 63 | "bamarni/composer-bin-plugin": "^1.4", 64 | "contao/manager-plugin": "^2.0", 65 | "contao/easy-coding-standard": "^3.0", 66 | "phpunit/phpunit": "^8.4 || ^9.5", 67 | "symfony/phpunit-bridge": "^4.4 || ^5.1", 68 | "phpstan/phpstan": "^0.12 || ^1.0", 69 | "phpstan/phpstan-phpunit": "^0.12 || ^1.0", 70 | "phpstan/phpstan-symfony": "^0.12 || ^1.0", 71 | "slam/phpstan-extensions": "^4.0 || ^5.1 || ^6.0", 72 | "thecodingmachine/phpstan-strict-rules": "^0.12 || ^1.0" 73 | }, 74 | "autoload": { 75 | "psr-4": { 76 | "Pdir\\SocialFeedBundle\\": "src/" 77 | } 78 | }, 79 | "autoload-dev": { 80 | "psr-4": { 81 | "Pdir\\SocialFeedBundle\\Tests\\": "tests/" 82 | } 83 | }, 84 | "funding": [ 85 | { 86 | "type": "patreon", 87 | "url": "https://www.patreon.com/user?u=28241718" 88 | }, 89 | { 90 | "type": "corporate", 91 | "url": "https://contao-themes.net/sponsoring.html" 92 | } 93 | ], 94 | "suggest": { 95 | "pdir/social-feed-plus-bundle": "Publish news, events or regular pages on all available social media channels at once or do this manually with one click / News, Events oder regulären Seiten auf allen verfügbaren Social Media Kanälen auf einmal veröffentlichen oder dies manuell mit einem Klick tun.", 96 | "contao-themes-net/mate-theme-bundle": "MATE Theme includes all styles for social feed bundle. / MATE Theme enthält Stylesheets für das Social Feed Bundle.", 97 | "contao-themes-net/odd-theme-bundle": "ODD Theme includes all styles for social feed bundle. / ODD Theme enthält Stylesheets für das Social Feed Bundle.", 98 | "contao-themes-net/nature-theme-bundle": "NATURE Theme includes all styles for social feed bundle. / NATURE Theme enthält Stylesheets für das Social Feed Bundle.", 99 | "contao-themes-net/zero-one-theme-bundle": "Show a social feed in the 0.1 Theme. / Zeige einen Social Feed im 0.1 Theme an." 100 | }, 101 | "extra": { 102 | "contao-manager-plugin": "Pdir\\SocialFeedBundle\\ContaoManager\\Plugin" 103 | }, 104 | "config": { 105 | "allow-plugins": { 106 | "contao-components/installer": true, 107 | "dealerdirect/phpcodesniffer-composer-installer": true, 108 | "contao/manager-plugin": true, 109 | "bamarni/composer-bin-plugin": true, 110 | "php-http/discovery": true, 111 | "contao-community-alliance/composer-plugin": true 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /public/css/social_feed.css: -------------------------------------------------------------------------------- 1 | .social_feed_element { 2 | overflow: visible; 3 | padding: 0; 4 | width: 100%; 5 | } 6 | .social_feed_element.extended .inner a { 7 | display: inline; 8 | padding: 0; 9 | color: rgb(27, 149, 224); 10 | } 11 | .social_feed_element.extended .inner a:hover { 12 | text-decoration: underline; 13 | } 14 | .social_feed_element.extended .inner > figure { 15 | padding: 0; 16 | } 17 | .social_feed_element.extended a.more { 18 | padding: 0; 19 | font-size: 14px; 20 | color: rgb(27, 149, 224); 21 | font-weight: 700; 22 | display: block; 23 | } 24 | .social_feed_element.extended a.more:hover { 25 | text-decoration: underline; 26 | } 27 | .social_feed_element.extended .inner { 28 | padding: 0 20px 20px; 29 | } 30 | .social_feed_element.extended p:first-child { 31 | margin-top: 0; 32 | } 33 | .social_feed_element.extended p:last-child { 34 | margin-bottom: 0; 35 | } 36 | .social_feed_element .inner { 37 | background: #f2f2f2; 38 | padding: 0; 39 | margin: 30px 10px 10px; 40 | position: relative; 41 | word-break: break-word; 42 | border-top: 5px solid #c1c1c1; 43 | } 44 | .social_feed_element .inner a { 45 | padding: 0 20px; 46 | display: block; 47 | } 48 | .social_feed_element .inner > figure { 49 | padding: 0 20px; 50 | } 51 | .social_feed_element .ce_text { 52 | padding: 15px 0; 53 | color: #333; 54 | margin: 0; 55 | } 56 | .social_feed_element .icon { 57 | position: static; 58 | text-indent: 0; 59 | } 60 | .social_feed_element .icon img { 61 | position: absolute; 62 | top: -20px; 63 | border-radius: 100%; 64 | max-width: 50px; 65 | } 66 | .social_feed_element .icon .image-wrapper:not(.loaded) { 67 | height: 0 !important; 68 | } 69 | .social_feed_element .info { 70 | margin: 0; 71 | padding: 10px 0 0; 72 | text-align: right; 73 | font-size: 14px; 74 | color: #989898; 75 | display: flex; 76 | align-items: center; 77 | justify-content: flex-end; 78 | } 79 | .social_feed_element .info img { 80 | width: 15px; 81 | margin-left: 10px; 82 | } 83 | .social_feed_element .title { 84 | margin: 10px 0; 85 | font-size: 16px; 86 | font-weight: 700; 87 | color: #333; 88 | } 89 | .social_feed_element a { 90 | color: #333; 91 | text-decoration: none; 92 | } 93 | .social_feed_element .ce_text { 94 | font-size: 14px; 95 | } 96 | .social_feed_element .fa { 97 | font-size: 18px; 98 | padding-left: 5px; 99 | } 100 | .social_feed_element .image_container > a { 101 | padding: 0; 102 | } 103 | 104 | .social_feed_container { 105 | width: calc(100% + 30px); 106 | margin-left: -15px; 107 | font-family: sans-serif; 108 | } 109 | .social_feed_container:not(.masonry) { 110 | display: -ms-flexbox; 111 | display: flex; 112 | -ms-flex-wrap: wrap; 113 | flex-wrap: wrap; 114 | } 115 | .social_feed_container:not(.masonry).columns2 .social_feed_element { 116 | -ms-flex: 0 0 50%; 117 | flex: 0 0 50%; 118 | } 119 | .social_feed_container:not(.masonry).columns3 .social_feed_element { 120 | -ms-flex: 0 0 33%; 121 | flex: 0 0 33%; 122 | } 123 | .social_feed_container:not(.masonry).columns4 .social_feed_element { 124 | -ms-flex: 0 0 25%; 125 | flex: 0 0 25%; 126 | } 127 | .social_feed_container.masonry.columns2 .social_feed_element, .social_feed_container.masonry.columns3 .social_feed_element, .social_feed_container.masonry.columns4 .social_feed_element { 128 | float: left; 129 | } 130 | .social_feed_container.masonry.columns2 .social_feed_element { 131 | width: 50%; 132 | } 133 | .social_feed_container.masonry.columns3 .social_feed_element { 134 | width: 33.33%; 135 | } 136 | .social_feed_container.masonry.columns4 .social_feed_element { 137 | width: 25%; 138 | } 139 | 140 | @media (max-width: 767px) { 141 | .social_feed_container { 142 | width: 100%; 143 | margin-left: 0; 144 | } 145 | .social_feed_container:not(.masonry).columns2 .social_feed_element, .social_feed_container:not(.masonry).columns3 .social_feed_element, .social_feed_container:not(.masonry).columns4 .social_feed_element { 146 | -ms-flex: 0 0 100%; 147 | flex: 0 0 100%; 148 | } 149 | .social_feed_container.masonry.columns2 .social_feed_element, .social_feed_container.masonry.columns3 .social_feed_element, .social_feed_container.masonry.columns4 .social_feed_element { 150 | float: none; 151 | width: 100%; 152 | } 153 | } 154 | 155 | /*# sourceMappingURL=social_feed.css.map */ 156 | -------------------------------------------------------------------------------- /src/Importer/Importer.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Importer; 22 | 23 | use Contao\Date; 24 | use Contao\Message; 25 | use Contao\System; 26 | use Pdir\SocialFeedBundle\Model\SocialFeedModel; 27 | 28 | class Importer 29 | { 30 | /** 31 | * @var InstagramClient 32 | */ 33 | protected $client; 34 | 35 | /* 36 | * account image uuid 37 | */ 38 | private $accountImage; 39 | 40 | /** 41 | * Collect data from the instagram api and return array. 42 | * 43 | * @throws \RuntimeException 44 | * 45 | * @return void|array 46 | */ 47 | public function getInstagramPosts($accessToken, $socialFeedId, $numberPosts = 30) 48 | { 49 | if (null === $accessToken) { 50 | Message::addError($GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['facebookNotSupported']); 51 | 52 | return []; 53 | } 54 | 55 | $client = System::getContainer()->get(InstagramClient::class); 56 | $items = $client->getMediaData($accessToken, (int) $socialFeedId, (int) $numberPosts); 57 | 58 | return $items['data']; 59 | } 60 | 61 | public function getAccountImage() 62 | { 63 | return $this->accountImage; 64 | } 65 | 66 | /** 67 | * Collect data from the instagram api and return array. 68 | * 69 | * @return void|array 70 | */ 71 | public function getInstagramAccount($accessToken, $socialFeedId) 72 | { 73 | $client = System::getContainer()->get(InstagramClient::class); 74 | 75 | return $client->getUserData($accessToken, (int) $socialFeedId); 76 | } 77 | 78 | /** 79 | * Collect data from the instagram api and return array. 80 | * 81 | * @return void|array 82 | */ 83 | public function getInstagramAccountImage($accessToken, $socialFeedId) 84 | { 85 | $client = System::getContainer()->get(InstagramClient::class); 86 | 87 | return $client->getUserImage($accessToken, (int) $socialFeedId, false); 88 | } 89 | 90 | public function moderation($items) 91 | { 92 | $listItems = []; 93 | 94 | foreach ($items as $item) { 95 | $image = ''; 96 | 97 | if (isset($item['media_url'])) { 98 | $image = false !== strpos($item['media_url'], 'jpg') ? $item['media_url'] : $item['thumbnail_url']; 99 | } 100 | 101 | if (!isset($item['media_url']) && isset($item['children']['data'][0]['media_url'])) { 102 | $image = $item['children']['data'][0]['media_url']; 103 | } 104 | 105 | $listItems[] = [ 106 | 'id' => $item['id'], 107 | 'title' => $item['caption'], 108 | 'time' => Date::parse($GLOBALS['TL_CONFIG']['datimFormat'], strtotime($item['timestamp'])), 109 | 'image' => $image, 110 | 'link' => $item['permalink'], 111 | ]; 112 | } 113 | 114 | return $listItems; 115 | } 116 | 117 | public function getPostsByAccount($id, $numberPosts) 118 | { 119 | $objSocialFeed = SocialFeedModel::findBy('id', $id); 120 | 121 | if (null === $objSocialFeed) { 122 | return; 123 | } 124 | 125 | switch ($objSocialFeed->socialFeedType) { 126 | case 'Facebook': 127 | Message::addError($GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['facebookNotSupported']); 128 | break; 129 | 130 | case 'Instagram': 131 | return $this->getInstagramPosts($objSocialFeed->psf_instagramAccessToken, $objSocialFeed->id, $numberPosts); 132 | break; 133 | 134 | case 'Twitter': 135 | Message::addError($GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['twitterNotSupported']); 136 | break; 137 | 138 | case 'LinkedIn': 139 | Message::addError($GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['linkedInNotSupported']); 140 | break; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /contao/dca/tl_news.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | use Contao\ArrayUtil; 22 | use Contao\Backend; 23 | use Contao\Config; 24 | use Contao\CoreBundle\DataContainer\PaletteManipulator; 25 | use Contao\DataContainer; 26 | 27 | /* 28 | * add global operation 29 | */ 30 | ArrayUtil::arrayInsert($GLOBALS['TL_DCA']['tl_news']['list']['global_operations'], 0, [ 31 | 'sf_moderate' => [ 32 | 'label' => &$GLOBALS['TL_LANG']['tl_news']['sf_moderate'], 33 | 'href' => 'key=moderate', 34 | 'class' => 'header_sf_moderate', 35 | 'attributes' => 'onclick="Backend.getScrollOffset()"', 36 | ], 37 | ]); 38 | 39 | /* 40 | * Add palette to tl_module 41 | */ 42 | 43 | $GLOBALS['TL_DCA']['tl_news']['fields']['social_feed_id'] = [ 44 | 'label' => &$GLOBALS['TL_LANG']['tl_news']['social_feed_id'], 45 | 'exclude' => true, 46 | 'inputType' => 'text', 47 | 'eval' => [ 48 | 'mandatory' => false, 49 | 'tl_class' => 'w50', 50 | 'decodeEntities' => true, 51 | ], 52 | 'sql' => "varchar(128) NOT NULL default ''", 53 | ]; 54 | 55 | $GLOBALS['TL_DCA']['tl_news']['fields']['social_feed_type'] = [ 56 | 'label' => &$GLOBALS['TL_LANG']['tl_news']['social_feed_type'], 57 | 'exclude' => true, 58 | 'filter' => true, 59 | 'sorting' => true, 60 | 'inputType' => 'select', 61 | 'options' => ['Facebook', 'Instagram', 'Twitter', 'LinkedIn'], 62 | 'eval' => ['includeBlankOption' => true, 'tl_class' => 'w50'], 63 | 'sql' => "varchar(255) NOT NULL default ''", 64 | ]; 65 | 66 | $GLOBALS['TL_DCA']['tl_news']['fields']['social_feed_account'] = [ 67 | 'label' => &$GLOBALS['TL_LANG']['tl_news']['social_feed_account'], 68 | 'exclude' => true, 69 | 'inputType' => 'text', 70 | 'eval' => [ 71 | 'mandatory' => false, 72 | 'tl_class' => 'w50', 73 | 'decodeEntities' => true, 74 | ], 75 | 'sql' => "varchar(128) NOT NULL default ''", 76 | ]; 77 | 78 | $GLOBALS['TL_DCA']['tl_news']['fields']['social_feed_account_picture'] = [ 79 | 'label' => &$GLOBALS['TL_LANG']['tl_news']['social_feed_account_picture'], 80 | 'exclude' => true, 81 | 'inputType' => 'fileTree', 82 | 'eval' => ['filesOnly' => true, 'fieldType' => 'radio', 'feEditable' => true, 'feViewable' => true, 'feGroup' => 'personal', 'tl_class' => 'w50 autoheight'], 83 | 'load_callback' => [ 84 | ['tl_news_socialfeed', 'setSingleSrcFlags'], 85 | ], 86 | 'sql' => 'binary(16) NULL', 87 | ]; 88 | 89 | $GLOBALS['TL_DCA']['tl_news']['fields']['social_feed_config'] = [ 90 | 'label' => &$GLOBALS['TL_LANG']['tl_news']['social_feed_config'], 91 | 'exclude' => true, 92 | 'inputType' => 'text', 93 | 'eval' => [ 94 | 'mandatory' => false, 95 | 'tl_class' => 'w50', 96 | 'readonly' => 'readonly', 97 | ], 98 | 'sql' => 'int(10) unsigned NULL', 99 | ]; 100 | 101 | class tl_news_socialfeed extends Backend 102 | { 103 | /** 104 | * Dynamically add flags to the "singleSRC" field. 105 | * 106 | * @param mixed $varValue 107 | * 108 | * @return mixed 109 | */ 110 | public function setSingleSrcFlags(mixed $varValue, DataContainer $dc): mixed 111 | { 112 | if ($dc->activeRecord) { 113 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['eval']['extensions'] = Config::get('validImageTypes'); 114 | } 115 | 116 | return $varValue; 117 | } 118 | } 119 | 120 | foreach ($GLOBALS['TL_DCA']['tl_news']['palettes'] as $name => $palette) { 121 | if (!is_string($palette)) { 122 | continue; 123 | } 124 | 125 | PaletteManipulator::create() 126 | ->addLegend('pdir_sf_settings_legend', 'publish_legend', PaletteManipulator::POSITION_AFTER) 127 | ->addField('social_feed_type', 'pdir_sf_settings_legend', PaletteManipulator::POSITION_APPEND) 128 | ->addField('social_feed_id', 'social_feed_type', PaletteManipulator::POSITION_AFTER) 129 | ->addField('social_feed_account', 'social_feed_id', PaletteManipulator::POSITION_AFTER) 130 | ->addField('social_feed_account_picture', 'social_feed_account', PaletteManipulator::POSITION_AFTER) 131 | ->applyToPalette($name, 'tl_news') 132 | ; 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Social Feed extension for Contao 2 | ============================================================ 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/pdir/social-feed-bundle/v/stable)](https://packagist.org/packages/pdir/social-feed-bundle) 5 | [![Total Downloads](https://poser.pugx.org/pdir/social-feed-bundle/downloads)](https://packagist.org/packages/pdir/social-feed-bundle) 6 | [![License](https://poser.pugx.org/pdir/social-feed-bundle/license)](https://packagist.org/packages/pdir/social-feed-bundle) 7 | 8 | > [!TIP] 9 | > Social Feed+ version 10 | 11 | | [![Social Feed Plus](https://pdir.de/assets/images/f/pdir_icon_socialfeed_plus-0c93e4f1.svg)](https://pdir.de/socialfeed+) | With the paid Social Feed+ version, you can publish your news, events or regular pages to all available social media channels at once or do it manually with one click. Use Contao's own on-board tools to schedule your posts for Facebook, X ehemals Twitter, Instagram or LinkedIn and publish them automatically to all connected channels 10 minutes after publishing the news, event or a subpage. | 12 | |:--------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| 13 | | | [**Screenshots**](https://pdir.de/socialfeed+#screenshots)
[Kaufen](https://pdir.de/socialfeed+#buy) | 14 | 15 | ------------------------- 16 | 17 | About the free version 18 | ----- 19 | 20 | The Social Feed Extension shows a user feed from the most popular social 21 | networks (Facebook, Instagram, X formerly known as Twitter and LinkedIn). The posts are written directly 22 | into the database, created as news and can then displayed on the website 23 | using the module type news list. Since version 2.5.0 modaration of posts 24 | in news archive for instagram is available. 25 | 26 | **Deutsch** 27 | 28 | Die Social Feed Erweiterung zeigt einen Feed aus den beliebtesten sozialen 29 | Netzwerken an. Zurzeit werden Facebook, Instagram, X ehemals Twitter und LinkedIn unterstützt, 30 | weitere Kanäle folgen in Zukunft. Die Posts werden direkt in die Datenbank 31 | geschrieben, als News angelegt und können anschließend mit dem Modultyp 32 | Nachrichtenliste auf der Webseite angezeigt werden. Seit Version 2.5.0 33 | kannst du Instagram Beiträge direkt im News Archiv auswählen und entscheiden 34 | welche Beiträge importiert werden sollen. 35 | 36 | 37 | Auf [contao-themes.net](https://contao-themes.net/sponsoring.html?isorc=3) können Sie die Weiterentwicklung unserer Themes und Erweiterungen durch das Kaufen von speziellen Paketen oder das Spenden von Entwicklungsstunden unterstützen. 38 | 39 | 40 | Screenshot 41 | ----------- 42 | ![Social Feed Stream](https://pdir.de/files/pdir/01_inhalte/social_feed_demo.png "Social Feed Stream Example") 43 | 44 | ![Moderate Instagram](https://pdir.de/files/pdir/01_inhalte/moderiere-instagram-im-backend.png "Moderate Instagram Example") 45 | 46 | System requirements 47 | ------------------- 48 | 49 | * [Contao 4.3](https://github.com/contao/contao-bundle) or higher 50 | 51 | Installation & Configuration 52 | ---------------------------- 53 | * [Dokumentation](https://pdir.de/docs/de/contao/extensions/socialfeed/) 54 | 55 | Commands 56 | ---------------------------- 57 | 58 | php vendor/bin/contao-console linkedin:import 59 | php vendor/bin/contao-console linkedin:import -d true -m 30 60 | 61 | 62 | Demo 63 | ---------------------------- 64 | * [Social Feed Demo](https://demo.pdir.de/social-feed.html) 65 | 66 | Dependencies 67 | ------------ 68 | 69 | - [nickdnk/graph-sdk](https://github.com/nickdnk/php-graph-sdk) 70 | - [abraham/twitteroauth](https://github.com/abraham/twitteroauth) 71 | - [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) 72 | - [zoonman/linkedin-api-php-client](https://github.com/zoonman/linkedin-api-php-client) 73 | 74 | License 75 | ------- 76 | GNU Lesser General Public License v3.0 77 | Font Awesome Free License 78 | 79 | See LICENSE files in package root. 80 | 81 | Developing & Pull Request 82 | ------- 83 | 84 | Run the PHP-CS-Fixer and the unit tests before you make a pull request to the bundle: 85 | 86 | vendor/bin/ecs check src tests --ansi 87 | vendor/bin/phpunit 88 | vendor/bin/phpstan analyse --no-progress --ansi 89 | -------------------------------------------------------------------------------- /src/SocialFeed/SocialFeedNewsClass.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\SocialFeed; 22 | 23 | use Contao\FilesModel; 24 | use Contao\StringUtil; 25 | use Contao\System; 26 | use Pdir\SocialFeedBundle\Model\SocialFeedModel; 27 | 28 | class SocialFeedNewsClass 29 | { 30 | private string|array|bool|int|null|float $projectDir; 31 | private $staticUrl; 32 | 33 | public function parseNews($objTemplate, $arrRow, $objModule): void 34 | { 35 | if ('' !== $arrRow['social_feed_id']) { 36 | 37 | $container = System::getContainer(); 38 | $this->projectDir = $container->getParameter('kernel.project_dir'); 39 | $this->staticUrl = $container->get('contao.assets.files_context')->getStaticUrl(); 40 | 41 | $teaser = $arrRow['teaser']; 42 | 43 | if ($objModule->pdir_sf_text_length > 0 && null !== $teaser) { 44 | $more = ''; 45 | 46 | if (\strlen($teaser) > $objModule->pdir_sf_text_length) { 47 | $more = ' ...'; 48 | } 49 | $teaser = StringUtil::substrHtml($teaser, $objModule->pdir_sf_text_length).$more; 50 | } 51 | 52 | $objTemplate->sfFbAccountPicture = $arrRow['social_feed_account_picture']; 53 | $objTemplate->sfTextLength = $objModule->pdir_sf_text_length; 54 | $objTemplate->sfElementWidth = $objModule->pdir_sf_columns; 55 | $objTemplate->sfImages = $objModule->pdir_sf_enableImages; 56 | $objTemplate->teaser = $teaser; 57 | $objTemplate->socialFeedType = $arrRow['social_feed_type']; 58 | 59 | $pictureFactory = System::getContainer()->get('contao.image.picture_factory'); 60 | 61 | if (null !== $arrRow['social_feed_account_picture']) { 62 | $imagePath = FilesModel::findByUuid($arrRow['social_feed_account_picture'])->path?? null; 63 | 64 | if (null === $imagePath) { 65 | $objTemplate->accountPicture = ''; 66 | } 67 | 68 | if (null !== $imagePath) { 69 | $pictureObj = $pictureFactory->create($this->projectDir.DIRECTORY_SEPARATOR.$imagePath); 70 | 71 | if (null !== $pictureObj) { 72 | $objTemplate->accountPicture = $this->getTemplateData($pictureObj); 73 | } 74 | } 75 | } else { 76 | $socialFeedAccount = SocialFeedModel::findBy('id', $arrRow['social_feed_config']); 77 | 78 | if (null !== $socialFeedAccount->instagram_account_picture) { 79 | $image = FilesModel::findByUuid($socialFeedAccount->instagram_account_picture); 80 | $size = StringUtil::deserialize($socialFeedAccount->instagram_account_picture_size); 81 | $objTemplate->accountPicture = $this->getTemplateData($pictureFactory->create($this->projectDir.DIRECTORY_SEPARATOR.$image->path, $size)); 82 | } elseif (null !== $socialFeedAccount->linkedin_account_picture) { 83 | $image = FilesModel::findByUuid($socialFeedAccount->linkedin_account_picture); 84 | $size = StringUtil::deserialize($socialFeedAccount->linkedin_account_picture_size); 85 | $objTemplate->accountPicture = $this->getTemplateData($pictureFactory->create($this->projectDir.DIRECTORY_SEPARATOR.$image->path, $size)); 86 | } 87 | } 88 | 89 | if (null !== $arrRow['social_feed_account'] && '' !== $arrRow['social_feed_account']) { 90 | $objTemplate->sfFbAccount = $arrRow['social_feed_account']; 91 | } else { 92 | $socialFeedAccount = SocialFeedModel::findBy('id', $arrRow['social_feed_config']); 93 | $objTemplate->sfFbAccount = $socialFeedAccount->instagram_account; 94 | } 95 | } 96 | } 97 | 98 | private function getTemplateData($picture) 99 | { 100 | if (null === $picture) { 101 | return; 102 | } 103 | 104 | return [ 105 | 'img' => $picture->getImg($this->projectDir, $this->staticUrl), 106 | 'sources' => $picture->getSources($this->projectDir, $this->staticUrl), 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Importer/NewsImporter.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Importer; 22 | 23 | use Contao\Dbafs; 24 | use Contao\File; 25 | use Contao\FilesModel; 26 | use Contao\Folder; 27 | use Contao\NewsArchiveModel; 28 | use Contao\NewsModel; 29 | use Contao\System; 30 | use Pdir\SocialFeedBundle\Model\SocialFeedModel; 31 | 32 | class NewsImporter 33 | { 34 | public $accountImage; 35 | protected $arrNews; 36 | 37 | public function execute($newsArchiveId, SocialFeedModel $socialFeedAccount): void 38 | { 39 | $objNews = new NewsModel(); 40 | 41 | // check if news exists 42 | if (null !== $objNews->findBy('social_feed_id', $this->arrNews['id'])) { 43 | return; 44 | } 45 | 46 | $objNews->pid = $newsArchiveId; 47 | 48 | // social feed 49 | $objNews->social_feed_type = $socialFeedAccount->socialFeedType; 50 | $objNews->social_feed_id = $this->arrNews['id']; 51 | $objNews->social_feed_config = $socialFeedAccount->id; 52 | 53 | // post image 54 | $objNews->singleSRC = $this->arrNews['singleSRC']; 55 | 56 | if (!empty($objNews->singleSRC)) { 57 | $objNews->addImage = 1; 58 | } 59 | 60 | // account image 61 | // $accountPicturePath = $imgPath . $socialFeedAccount->id . '.jpg'; 62 | // $accountPictureUuid = $this->saveImage($accountPicturePath, $this->accountImage); 63 | // $objNews->social_feed_account_picture = $accountPictureUuid; 64 | 65 | // headline and teaser 66 | $objNews->headline = $this->arrNews['headline']; 67 | 68 | // set headline to id if headline is not set 69 | if ('' === $objNews->headline) { 70 | $objNews->headline = $this->arrNews['id']; 71 | } 72 | 73 | $objNews->teaser = $this->arrNews['teaser']; 74 | 75 | // author 76 | $objNews->author = $socialFeedAccount->user; 77 | 78 | // default 79 | $objNews->published = 1; 80 | $objNews->source = 'external'; 81 | $objNews->target = 1; 82 | $objNews->url = $this->arrNews['permalink']; 83 | $objNews->tstamp = time(); 84 | $objNews->date = $this->arrNews['date']; 85 | $objNews->time = $this->arrNews['time']; 86 | $objNews->alias = $this->generateAlias($objNews->headline, $objNews->pid); 87 | 88 | if (null !== NewsModel::findOneByAlias($objNews->alias)) { 89 | $objNews->alias .= '-'.$this->arrNews['id']; 90 | } 91 | 92 | // save the news 93 | $objNews->save(); 94 | } 95 | 96 | public function setNews($arr): void 97 | { 98 | $this->arrNews = $arr; 99 | } 100 | 101 | public static function createImageFolder($account): string 102 | { 103 | // Create Public Image Folder 104 | $imgPath = 'files/social-feed/'.$account.'/'; 105 | 106 | if (!file_exists($imgPath)) { 107 | new Folder($imgPath); 108 | $file = new File('files/social-feed/.public'); 109 | $file->write(''); 110 | $file->close(); 111 | } 112 | 113 | return $imgPath; 114 | } 115 | 116 | public static function saveImage($strPath, $strUrl): ?string 117 | { 118 | if (!file_exists($strPath)) { 119 | $strImage = file_get_contents($strUrl); 120 | $file = new File($strPath); 121 | $file->write($strImage); 122 | $file->close(); 123 | 124 | // add resource 125 | $objFile = Dbafs::addResource($file->path); 126 | 127 | return $objFile->uuid; 128 | } 129 | 130 | // use existing file 131 | $objFile = FilesModel::findByPath($strPath); 132 | 133 | return $objFile->uuid; 134 | } 135 | 136 | public function generateAlias($headline, $newsArchiveId) 137 | { 138 | return System::getContainer()->get('contao.slug')->generate($headline, NewsArchiveModel::findById($newsArchiveId)->jumpTo); 139 | } 140 | 141 | public static function shortenHeadline($str): string 142 | { 143 | $arr = explode("\n", $str); 144 | 145 | $message = $arr[0] ?? $str; 146 | $more = ''; 147 | 148 | if (\strlen($message) > 50) { 149 | $more = ' ...'; 150 | } 151 | 152 | return mb_substr($message, 0, 50).$more; 153 | } 154 | 155 | public static function setLastImportDate(SocialFeedModel $socialFeedModel): void 156 | { 157 | $socialFeedModel->pdir_sf_fb_news_last_import_date = time(); 158 | $socialFeedModel->save(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /public/js/imagesloaded.pkgd.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * imagesLoaded PACKAGED v4.1.4 3 | * JavaScript is all like "You images are done yet or what?" 4 | * MIT License 5 | */ 6 | 7 | !function(e,t){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",t):"object"==typeof module&&module.exports?module.exports=t():e.EvEmitter=t()}("undefined"!=typeof window?window:this,function(){function e(){}var t=e.prototype;return t.on=function(e,t){if(e&&t){var i=this._events=this._events||{},n=i[e]=i[e]||[];return n.indexOf(t)==-1&&n.push(t),this}},t.once=function(e,t){if(e&&t){this.on(e,t);var i=this._onceEvents=this._onceEvents||{},n=i[e]=i[e]||{};return n[t]=!0,this}},t.off=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){var n=i.indexOf(t);return n!=-1&&i.splice(n,1),this}},t.emitEvent=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){i=i.slice(0),t=t||[];for(var n=this._onceEvents&&this._onceEvents[e],o=0;o 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Controller; 22 | 23 | use Contao\BackendUser; 24 | use Contao\System; 25 | use Doctrine\DBAL\Connection; 26 | use Pdir\SocialFeedBundle\EventListener\DataContainer\SocialFeedListener; 27 | use Pdir\SocialFeedBundle\Importer\InstagramClient; 28 | use Symfony\Component\HttpFoundation\RedirectResponse; 29 | use Symfony\Component\HttpFoundation\Request; 30 | use Symfony\Component\HttpFoundation\Response; 31 | use Symfony\Component\HttpFoundation\Session\SessionInterface; 32 | use Symfony\Component\Routing\Annotation\Route; 33 | use Symfony\Component\Routing\RouterInterface; 34 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 35 | 36 | #[Route('_instagram', defaults: ['_scope' => 'backend', '_token_check' => false])] 37 | class InstagramController 38 | { 39 | /** 40 | * @var InstagramClient 41 | */ 42 | private $client; 43 | 44 | /** 45 | * @var Connection 46 | */ 47 | private $db; 48 | 49 | /** 50 | * @var RouterInterface 51 | */ 52 | private $router; 53 | 54 | /** 55 | * @var SessionInterface 56 | */ 57 | private $session; 58 | 59 | /** 60 | * @var TokenStorageInterface 61 | */ 62 | private $tokenStorage; 63 | 64 | /** 65 | * InstagramController constructor. 66 | */ 67 | public function __construct(InstagramClient $client, Connection $db, RouterInterface $router, TokenStorageInterface $tokenStorage) 68 | { 69 | $this->client = $client; 70 | $this->db = $db; 71 | $this->router = $router; 72 | $this->session = System::getContainer()->get('request_stack')->getCurrentRequest()->getSession(); 73 | $this->tokenStorage = $tokenStorage; 74 | } 75 | 76 | #[Route('/auth', name: 'instagram_auth', methods: ['GET'])] 77 | public function authAction(Request $request): Response 78 | { 79 | // Missing code query parameter 80 | if (!($code = $request->query->get('code'))) { 81 | return new Response(Response::$statusTexts[Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); 82 | } 83 | 84 | // User not logged in 85 | if (null === $this->getBackendUser()) { 86 | return new Response(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED); 87 | } 88 | 89 | $sessionData = $this->session->get(SocialFeedListener::SESSION_KEY); 90 | 91 | // Social feed ID not found in session 92 | if (null === $sessionData || !isset($sessionData['socialFeedId'])) { 93 | return new Response(Response::$statusTexts[Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); 94 | } 95 | 96 | // Social feed account not found 97 | if (false === ($module = $this->db->fetchAssociative('SELECT * FROM tl_social_feed WHERE id=?', [$sessionData['socialFeedId']]))) { 98 | return new Response(Response::$statusTexts[Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); 99 | } 100 | 101 | $longLivedAccessToken = $this->client->getAccessToken( 102 | $module['psf_instagramAppId'], 103 | $module['psf_instagramAppSecret'], 104 | $code, 105 | $this->router->generate('instagram_auth', [], RouterInterface::ABSOLUTE_URL) 106 | ); 107 | 108 | if (null === $longLivedAccessToken['access_token']) { 109 | return new Response(Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR], Response::HTTP_INTERNAL_SERVER_ERROR); 110 | } 111 | 112 | // Get the user and media data 113 | $this->client->getUserData($longLivedAccessToken['access_token'], (int) $module['id'], false); 114 | // $mediaData = $this->client->getMediaData($accessToken, (int) $module['id'], false); 115 | 116 | // Store the access token and remove temporary session key 117 | $this->db->update('tl_social_feed', ['psf_instagramAccessToken' => $longLivedAccessToken['access_token'], 'access_token_expires' => time() + $longLivedAccessToken['expires_in']], ['id' => $sessionData['socialFeedId']]); 118 | $this->session->remove(SocialFeedListener::SESSION_KEY); 119 | 120 | return new RedirectResponse($sessionData['backUrl']); 121 | } 122 | 123 | /** 124 | * Get the backend user. 125 | */ 126 | private function getBackendUser(): ?BackendUser 127 | { 128 | if (null === ($token = $this->tokenStorage->getToken())) { 129 | return null; 130 | } 131 | 132 | $user = $token->getUser(); 133 | 134 | if (!($user instanceof BackendUser)) { 135 | return null; 136 | } 137 | 138 | return $user; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Cron/RefreshAccessTokenCron.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Cron; 22 | 23 | use Contao\CoreBundle\Framework\ContaoFramework; 24 | use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob; 25 | use Contao\CoreBundle\Monolog\ContaoContext; 26 | use Contao\Database; 27 | use Contao\Email; 28 | use Contao\System; 29 | use GuzzleHttp\Client; 30 | use Pdir\SocialFeedBundle\Model\SocialFeedModel; 31 | use Psr\Log\LogLevel; 32 | 33 | #[AsCronJob('daily')] 34 | class RefreshAccessTokenCron 35 | { 36 | private ?object $logger; 37 | 38 | public function __construct(private ContaoFramework $framework) 39 | { 40 | } 41 | 42 | /** 43 | * @throws \Exception 44 | */ 45 | public function __invoke(): void 46 | { 47 | $this->framework->initialize(); 48 | $this->logger = System::getContainer()->get('monolog.logger.contao'); 49 | 50 | $objSocialFeed = SocialFeedModel::findAll(); 51 | 52 | if (null === $objSocialFeed) { 53 | return; 54 | } 55 | 56 | foreach ($objSocialFeed as $account) { 57 | // LinkedIn 58 | if ('LinkedIn' === $account->socialFeedType && '' !== $account->linkedin_access_token && '' !== $account->linkedin_refresh_token) { 59 | $this->refreshLinkedInAccessToken($account); 60 | } 61 | 62 | // Instagram 63 | if ('Instagram' === $account->socialFeedType && '' !== $account->psf_instagramAccessToken) { 64 | $this->refreshInstagramAccessToken($account); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * @throws \Exception 71 | */ 72 | private function refreshLinkedInAccessToken(SocialFeedModel $socialFeedModel): void 73 | { 74 | if (\strtotime('+1 week', \time()) >= $socialFeedModel->linkedin_refresh_token_expires || '' === $socialFeedModel->linkedin_refresh_token_expires) { 75 | $objMail = new Email(); 76 | $objMail->subject = $GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInSubject']; 77 | $objMail->html = sprintf($GLOBALS['TL_LANG']['BE_MOD']['emailLinkedInHtml'], $socialFeedModel->noteForRefreshTokenMail?? '-', $socialFeedModel->linkedin_company_id); 78 | $objMail->from = $GLOBALS['TL_CONFIG']['adminEmail']; 79 | $objMail->fromName = $socialFeedModel->noteForRefreshTokenMail?? '-'; 80 | $objMail->sendTo($GLOBALS['TL_CONFIG']['adminEmail']); 81 | } 82 | 83 | if ($socialFeedModel->access_token_expires <= \strtotime('+1 week', \time())) { 84 | $data = [ 85 | 'grant_type' => 'refresh_token', 86 | 'refresh_token' => $socialFeedModel->linkedin_refresh_token, 87 | 'client_id' => $socialFeedModel->linkedin_client_id, 88 | 'client_secret' => $socialFeedModel->linkedin_client_secret, 89 | ]; 90 | 91 | try { 92 | $token = json_decode(file_get_contents('https://www.linkedin.com/oauth/v2/accessToken?'.http_build_query($data))); 93 | 94 | // Store the access token 95 | $db = Database::getInstance(); 96 | $set = ['linkedin_access_token' => $token->access_token, 'access_token_expires' => time() + $token->expires_in, 'linkedin_refresh_token' => $token->refresh_token, 'linkedin_refresh_token_expires' => time() + $token->refresh_token_expires_in]; 97 | $db->prepare('UPDATE tl_social_feed %s WHERE id = ?')->set($set)->execute($socialFeedModel->id); 98 | } catch (\Exception $e) { 99 | $this->logger->log(LogLevel::ERROR, $e->getMessage(), ['contao' => new ContaoContext(__METHOD__, 'ERROR')]); 100 | } 101 | } 102 | } 103 | 104 | private function refreshInstagramAccessToken(SocialFeedModel $socialFeedModel): void 105 | { 106 | if (\strtotime('+1 week', \time()) >= $socialFeedModel->access_token_expires || '' === $socialFeedModel->access_token_expires) { 107 | $client = new Client(); 108 | $response = $client->get('https://graph.instagram.com/refresh_access_token', [ 109 | 'query' => [ 110 | 'grant_type' => 'ig_refresh_token', 111 | 'access_token' => $socialFeedModel->psf_instagramAccessToken, 112 | ], 113 | ]); 114 | 115 | try { 116 | $data = json_decode((string) $response->getBody(), true); 117 | 118 | // Store the access token 119 | $db = Database::getInstance(); 120 | $set = ['psf_instagramAccessToken' => $data['access_token'], 'access_token_expires' => time() + $data['expires_in']]; 121 | $db->prepare('UPDATE tl_social_feed %s WHERE id = ?')->set($set)->execute($socialFeedModel->id); 122 | } catch (\Exception $e) { 123 | $this->logger->log(LogLevel::ERROR, $e->getMessage(), ['contao' => new ContaoContext(__METHOD__, 'ERROR')]); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /public/img/pdir_icon_socialfeed_plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Cron/InstagramImportCron.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Cron; 22 | 23 | use Contao\CoreBundle\Framework\ContaoFramework; 24 | use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob; 25 | use Contao\CoreBundle\Monolog\ContaoContext; 26 | use Contao\Dbafs; 27 | use Contao\File; 28 | use Contao\NewsModel; 29 | use Contao\System; 30 | use Pdir\SocialFeedBundle\Importer\Importer; 31 | use Pdir\SocialFeedBundle\Importer\NewsImporter; 32 | use Pdir\SocialFeedBundle\Model\SocialFeedModel; 33 | use Psr\Log\LogLevel; 34 | 35 | #[AsCronJob('minutely')] 36 | class InstagramImportCron 37 | { 38 | use ImportCronHelperTrait; 39 | 40 | public int $counter = 0; 41 | public function __construct(private ContaoFramework $framework) 42 | { 43 | } 44 | 45 | /** 46 | * @throws FacebookSDKException 47 | * @throws \Exception 48 | */ 49 | public function __invoke(): void 50 | { 51 | $this->framework->initialize(); 52 | $logger = System::getContainer()->get('monolog.logger.contao'); 53 | 54 | if ($this->poorManCron) { 55 | $objSocialFeed = SocialFeedModel::findBy(['socialFeedType = ?', 'pdir_sf_fb_news_cronjob != ?'], ['Instagram', 'no_cronjob']); 56 | } else { 57 | $objSocialFeed = SocialFeedModel::findBy(['socialFeedType = ?', 'pdir_sf_fb_news_cronjob = ?'], ['Instagram', 'no_cronjob']); 58 | } 59 | 60 | if (null === $objSocialFeed) { 61 | return; 62 | } 63 | 64 | foreach ($objSocialFeed as $account) { 65 | $this->counter = 0; 66 | $cron = $account->pdir_sf_fb_news_cronjob; 67 | $lastImport = $account->pdir_sf_fb_news_last_import_date; 68 | 69 | if ('' === $lastImport) { 70 | $lastImport = 0; 71 | } 72 | $interval = \time() - $lastImport; 73 | 74 | if ($interval >= $cron || 0 === $lastImport || false === $this->poorManCron) { 75 | NewsImporter::setLastImportDate($account); 76 | 77 | $objImporter = new Importer(); 78 | 79 | // get instagram picture # not supported 80 | // $picture = $objImporter->getInstagramAccountImage($account->psf_instagramAccessToken, $account->id); 81 | 82 | // get instagram posts for account 83 | $medias = $objImporter->getInstagramPosts($account->psf_instagramAccessToken, $account->id, $account->number_posts); 84 | 85 | if (!\is_array($medias)) { 86 | continue; 87 | } 88 | 89 | foreach ($medias as $media) { 90 | $objNews = new NewsModel(); 91 | 92 | if (null !== $objNews->findBy('social_feed_id', $media['id'])) { 93 | continue; 94 | } 95 | 96 | $imgPath = NewsImporter::createImageFolder($account->id); 97 | 98 | // save pictures 99 | $picturePath = $imgPath.$media['id'].'.jpg'; 100 | $this->savePostPictures($picturePath, $media); 101 | 102 | // Write in Database 103 | $message = $media['caption']?? ''; 104 | 105 | // add/fetch file from DBAFS 106 | $objFile = Dbafs::addResource($imgPath.$media['id'].'.jpg'); 107 | $this->saveInstagramNews($objNews, $account, $objFile, $message, $media); 108 | 109 | $this->counter++; 110 | } 111 | 112 | if (0 < $this->counter) { 113 | $logger->log(LogLevel::INFO, 'Social Feed (ID '.$account->id.'): Instagram - imported ' . $this->counter . ' items.', ['contao' => new ContaoContext(__METHOD__, 'INFO')]); 114 | } 115 | } 116 | 117 | if (0 === $this->counter) { 118 | $logger->log(LogLevel::INFO, 'Social Feed (ID '.$account->id.'): Instagram Import - nothing to import', ['contao' => new ContaoContext(__METHOD__, 'INFO')]); 119 | } 120 | } 121 | } 122 | 123 | private function saveInstagramNews($objNews, $obj, $objFile, $message, $media, $account = '', $accountPicture = ''): void 124 | { 125 | $objNews->pid = $obj->pdir_sf_fb_news_archive; 126 | $objNews->author = $obj->user; 127 | $objNews->singleSRC = $objFile->uuid; 128 | $objNews->addImage = 1; 129 | $objNews->tstamp = time(); 130 | 131 | if ('' === $message) { 132 | $message = $media['id']; 133 | } 134 | 135 | $more = ''; 136 | if (null !== $message && \strlen($message) > 50) { 137 | $more = ' ...'; 138 | } 139 | 140 | $objNews->headline = mb_substr($message, 0, 50).$more; 141 | 142 | $message = str_replace("\n", '
', $message); 143 | $objNews->teaser = $message; 144 | $objNews->date = strtotime($media['timestamp']); 145 | $objNews->time = strtotime($media['timestamp']); 146 | $objNews->published = 1; 147 | $objNews->social_feed_type = $obj->socialFeedType; 148 | $objNews->social_feed_id = $media['id']; 149 | $objNews->social_feed_config = $obj->id; 150 | //$objNews->social_feed_account = $account; 151 | //$objNews->social_feed_account_picture = Dbafs::addResource($accountPicture)->uuid; 152 | $objNews->source = 'external'; 153 | $objNews->url = $media['permalink']; 154 | $objNews->target = 1; 155 | $objNews->save(); 156 | } 157 | 158 | /** 159 | * @throws \Exception 160 | */ 161 | private function savePostPictures($picturePath, $media): void 162 | { 163 | if (!file_exists($picturePath)) { 164 | $strImage = file_get_contents(false !== strpos($media['media_url'], 'jpg') ? $media['media_url'] : $media['thumbnail_url']); 165 | $file = new File($picturePath); 166 | $file->write($strImage); 167 | $file->close(); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/EventListener/DataContainer/SocialFeedListener.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\EventListener\DataContainer; 22 | 23 | use Contao\BackendUser; 24 | use Contao\CoreBundle\Exception\RedirectResponseException; 25 | use Contao\CoreBundle\Image\ImageSizes; 26 | use Contao\DataContainer; 27 | use Contao\Environment; 28 | use Contao\Input; 29 | use Contao\System; 30 | use Pdir\SocialFeedBundle\EventListener\Config; 31 | use Symfony\Component\HttpFoundation\Session\SessionInterface; 32 | use Symfony\Component\Routing\RouterInterface; 33 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 34 | 35 | class SocialFeedListener 36 | { 37 | public const SESSION_KEY = 'social-feed-id'; 38 | 39 | /** 40 | * @var SessionInterface 41 | */ 42 | private $session; 43 | 44 | /** 45 | * ModuleListener constructor. 46 | */ 47 | public function __construct( 48 | private readonly RouterInterface $router, 49 | private readonly TokenStorageInterface $tokenStorage, 50 | private readonly ImageSizes $imageSizes 51 | ) 52 | { 53 | $this->session = System::getContainer()->get('request_stack')->getCurrentRequest()->getSession(); 54 | } 55 | 56 | /** 57 | * On submit callback. 58 | */ 59 | public function onSubmitCallback(DataContainer $dc): void 60 | { 61 | if ('Instagram' === $dc->activeRecord->socialFeedType && $dc->activeRecord->psf_instagramAppId && Input::post('psf_instagramRequestToken')) { 62 | $this->requestAccessToken($dc->activeRecord->psf_instagramAppId); 63 | } 64 | 65 | if ('Facebook' === $dc->activeRecord->socialFeedType && $dc->activeRecord->pdir_sf_fb_app_id && $dc->activeRecord->pdir_sf_fb_app_secret && Input::post('psf_facebookRequestToken')) { 66 | $this->requestFbAccessToken($dc->activeRecord->pdir_sf_fb_app_id, $dc->activeRecord->pdir_sf_fb_app_secret, $dc->activeRecord->pdir_sf_fb_account); 67 | } 68 | 69 | if ('LinkedIn' === $dc->activeRecord->socialFeedType && $dc->activeRecord->linkedin_client_id && $dc->activeRecord->linkedin_client_secret && Input::post('linkedin_request_token')) { 70 | $this->requestLinkedinAccessToken($dc->activeRecord->linkedin_client_id, $dc->activeRecord->linkedin_client_secret); 71 | } 72 | } 73 | 74 | /** 75 | * On the request token save. 76 | */ 77 | public function onRequestTokenSave() 78 | { 79 | return null; 80 | } 81 | 82 | /** 83 | * @Callback(table="tl_social_feed", target="fields.linkedin_account_picture_size.options") 84 | * @Callback(table="tl_social_feed", target="fields.instagram_account_picture_size.options") 85 | */ 86 | public function getImageSizeOptions(): array 87 | { 88 | $user = $this->tokenStorage->getToken()?->getUser(); 89 | 90 | if (!$user instanceof BackendUser) { 91 | return []; 92 | } 93 | 94 | return $this->imageSizes->getOptionsForUser($user); 95 | } 96 | 97 | /** 98 | * Dynamically add flags to the "singleSRC" field. 99 | * 100 | * @Callback(table="tl_social_feed", target="fields.linkedin_account_picture.load") 101 | * @Callback(table="tl_social_feed", target="fields.instagram_account_picture.load") 102 | */ 103 | public function setSingleSrcFlags(mixed $varValue, DataContainer $dc): mixed 104 | { 105 | if ($dc->activeRecord && isset($dc->activeRecord->type)) { 106 | switch ($dc->activeRecord->type) { 107 | case 'text': 108 | case 'hyperlink': 109 | case 'image': 110 | case 'accordionSingle': 111 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['eval']['extensions'] = Config::get('validImageTypes'); 112 | break; 113 | 114 | case 'download': 115 | $GLOBALS['TL_DCA'][$dc->table]['fields'][$dc->field]['eval']['extensions'] = Config::get('allowedDownload'); 116 | break; 117 | } 118 | } 119 | 120 | return $varValue; 121 | } 122 | 123 | /** 124 | * Request the Instagram access token. 125 | * 126 | * @param string $clientId 127 | */ 128 | private function requestAccessToken($clientId): void 129 | { 130 | $this->session->set(self::SESSION_KEY, [ 131 | 'socialFeedId' => Input::get('id'), 132 | 'backUrl' => Environment::get('uri'), 133 | ]); 134 | 135 | $this->session->save(); 136 | 137 | $data = [ 138 | 'client_id' => $clientId, 139 | 'redirect_uri' => $this->router->generate('instagram_auth', [], RouterInterface::ABSOLUTE_URL), 140 | 'response_type' => 'code', 141 | 'scope' => 'instagram_business_basic', 142 | ]; 143 | 144 | throw new RedirectResponseException('https://www.instagram.com/oauth/authorize/?'.http_build_query($data)); 145 | } 146 | 147 | /** 148 | * Request the Facebook access token. 149 | * 150 | * @param string $appId 151 | */ 152 | private function requestFbAccessToken($appId, $appSecret, $page): void 153 | { 154 | $this->session->set(self::SESSION_KEY, [ 155 | 'socialFeedId' => Input::get('id'), 156 | 'backUrl' => Environment::get('uri'), 157 | 'clientId' => $appId, 158 | 'clientSecret' => $appSecret, 159 | 'page' => $page, 160 | ]); 161 | 162 | $this->session->save(); 163 | 164 | $data = [ 165 | 'client_id' => $appId, 166 | 'redirect_uri' => $this->router->generate('facebook_auth', [], RouterInterface::ABSOLUTE_URL), 167 | 'scope' => 'pages_read_engagement', 168 | ]; 169 | 170 | throw new RedirectResponseException('https://www.facebook.com/v11.0/dialog/oauth?'.http_build_query($data)); 171 | } 172 | 173 | /** 174 | * Request the LinkedIn access token. 175 | * 176 | * @param string $clientId 177 | * @param string $clientSecret 178 | */ 179 | private function requestLinkedinAccessToken($clientId, $clientSecret): void 180 | { 181 | $this->session->set(self::SESSION_KEY, [ 182 | 'socialFeedId' => Input::get('id'), 183 | 'backUrl' => Environment::get('uri'), 184 | 'clientId' => $clientId, 185 | 'clientSecret' => $clientSecret, 186 | ]); 187 | 188 | $this->session->save(); 189 | 190 | $data = [ 191 | 'response_type' => 'code', 192 | 'client_id' => $clientId, 193 | 'redirect_uri' => $this->router->generate('auth_linkedin', [], RouterInterface::ABSOLUTE_URL), 194 | 'scope' => 'r_organization_social,rw_organization_admin', 195 | ]; 196 | 197 | throw new RedirectResponseException('https://www.linkedin.com/oauth/v2/authorization?'.http_build_query($data)); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /LICENSE_FONT_AWESOME: -------------------------------------------------------------------------------- 1 | Fonticons, Inc. (https://fontawesome.com) 2 | 3 | -------------------------------------------------------------------------------- 4 | 5 | Font Awesome Free License 6 | 7 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 8 | commercial projects, open source projects, or really almost whatever you want. 9 | Full Font Awesome Free license: https://fontawesome.com/license/free. 10 | 11 | -------------------------------------------------------------------------------- 12 | 13 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 14 | 15 | The Font Awesome Free download is licensed under a Creative Commons 16 | Attribution 4.0 International License and applies to all icons packaged 17 | as SVG and JS file types. 18 | 19 | -------------------------------------------------------------------------------- 20 | 21 | # Fonts: SIL OFL 1.1 License 22 | 23 | In the Font Awesome Free download, the SIL OFL license applies to all icons 24 | packaged as web and desktop font files. 25 | 26 | Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com) 27 | with Reserved Font Name: "Font Awesome". 28 | 29 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 30 | This license is copied below, and is also available with a FAQ at: 31 | http://scripts.sil.org/OFL 32 | 33 | SIL OPEN FONT LICENSE 34 | Version 1.1 - 26 February 2007 35 | 36 | PREAMBLE 37 | The goals of the Open Font License (OFL) are to stimulate worldwide 38 | development of collaborative font projects, to support the font creation 39 | efforts of academic and linguistic communities, and to provide a free and 40 | open framework in which fonts may be shared and improved in partnership 41 | with others. 42 | 43 | The OFL allows the licensed fonts to be used, studied, modified and 44 | redistributed freely as long as they are not sold by themselves. The 45 | fonts, including any derivative works, can be bundled, embedded, 46 | redistributed and/or sold with any software provided that any reserved 47 | names are not used by derivative works. The fonts and derivatives, 48 | however, cannot be released under any other type of license. The 49 | requirement for fonts to remain under this license does not apply 50 | to any document created using the fonts or their derivatives. 51 | 52 | DEFINITIONS 53 | "Font Software" refers to the set of files released by the Copyright 54 | Holder(s) under this license and clearly marked as such. This may 55 | include source files, build scripts and documentation. 56 | 57 | "Reserved Font Name" refers to any names specified as such after the 58 | copyright statement(s). 59 | 60 | "Original Version" refers to the collection of Font Software components as 61 | distributed by the Copyright Holder(s). 62 | 63 | "Modified Version" refers to any derivative made by adding to, deleting, 64 | or substituting — in part or in whole — any of the components of the 65 | Original Version, by changing formats or by porting the Font Software to a 66 | new environment. 67 | 68 | "Author" refers to any designer, engineer, programmer, technical 69 | writer or other person who contributed to the Font Software. 70 | 71 | PERMISSION & CONDITIONS 72 | Permission is hereby granted, free of charge, to any person obtaining 73 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 74 | redistribute, and sell modified and unmodified copies of the Font 75 | Software, subject to the following conditions: 76 | 77 | 1) Neither the Font Software nor any of its individual components, 78 | in Original or Modified Versions, may be sold by itself. 79 | 80 | 2) Original or Modified Versions of the Font Software may be bundled, 81 | redistributed and/or sold with any software, provided that each copy 82 | contains the above copyright notice and this license. These can be 83 | included either as stand-alone text files, human-readable headers or 84 | in the appropriate machine-readable metadata fields within text or 85 | binary files as long as those fields can be easily viewed by the user. 86 | 87 | 3) No Modified Version of the Font Software may use the Reserved Font 88 | Name(s) unless explicit written permission is granted by the corresponding 89 | Copyright Holder. This restriction only applies to the primary font name as 90 | presented to the users. 91 | 92 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 93 | Software shall not be used to promote, endorse or advertise any 94 | Modified Version, except to acknowledge the contribution(s) of the 95 | Copyright Holder(s) and the Author(s) or with their explicit written 96 | permission. 97 | 98 | 5) The Font Software, modified or unmodified, in part or in whole, 99 | must be distributed entirely under this license, and must not be 100 | distributed under any other license. The requirement for fonts to 101 | remain under this license does not apply to any document created 102 | using the Font Software. 103 | 104 | TERMINATION 105 | This license becomes null and void if any of the above conditions are 106 | not met. 107 | 108 | DISCLAIMER 109 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 110 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 111 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 112 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 113 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 114 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 115 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 116 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 117 | OTHER DEALINGS IN THE FONT SOFTWARE. 118 | 119 | -------------------------------------------------------------------------------- 120 | 121 | # Code: MIT License (https://opensource.org/licenses/MIT) 122 | 123 | In the Font Awesome Free download, the MIT license applies to all non-font and 124 | non-icon files. 125 | 126 | Copyright 2022 Fonticons, Inc. 127 | 128 | Permission is hereby granted, free of charge, to any person obtaining a copy of 129 | this software and associated documentation files (the "Software"), to deal in the 130 | Software without restriction, including without limitation the rights to use, copy, 131 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 132 | and to permit persons to whom the Software is furnished to do so, subject to the 133 | following conditions: 134 | 135 | The above copyright notice and this permission notice shall be included in all 136 | copies or substantial portions of the Software. 137 | 138 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 139 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 140 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 141 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 142 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 143 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 144 | 145 | -------------------------------------------------------------------------------- 146 | 147 | # Attribution 148 | 149 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 150 | Awesome Free files already contain embedded comments with sufficient 151 | attribution, so you shouldn't need to do anything additional when using these 152 | files normally. 153 | 154 | We've kept attribution comments terse, so we ask that you do not actively work 155 | to remove them from files, especially code. They're a great way for folks to 156 | learn about Font Awesome. 157 | 158 | -------------------------------------------------------------------------------- 159 | 160 | # Brand Icons 161 | 162 | All brand icons are trademarks of their respective owners. The use of these 163 | trademarks does not indicate endorsement of the trademark holder by Font 164 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 165 | to represent the company, product, or service to which they refer.** 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- /src/Controller/ModerateController.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Philipp Seibt 15 | * @author pdir GmbH 16 | * 17 | * For the full copyright and license information, please view the LICENSE 18 | * file that was distributed with this source code. 19 | */ 20 | 21 | namespace Pdir\SocialFeedBundle\Controller; 22 | 23 | use Contao\BackendTemplate; 24 | use Contao\CoreBundle\Csrf\ContaoCsrfTokenManager; 25 | use Contao\CoreBundle\Framework\ContaoFramework; 26 | use Contao\Environment; 27 | use Contao\Input; 28 | use Contao\Message; 29 | use Contao\System; 30 | use Pdir\SocialFeedBundle\Importer\Importer; 31 | use Pdir\SocialFeedBundle\Importer\NewsImporter; 32 | use Pdir\SocialFeedBundle\Model\SocialFeedModel; 33 | use Symfony\Component\HttpFoundation\Request; 34 | use Symfony\Component\HttpFoundation\RequestStack; 35 | 36 | class ModerateController 37 | { 38 | private ContaoFramework $framework; 39 | 40 | private RequestStack $requestStack; 41 | 42 | private BackendTemplate $template; 43 | 44 | private string $message; 45 | private ContaoCsrfTokenManager $csrfTokenManager; 46 | 47 | /** 48 | * ExportController constructor. 49 | */ 50 | public function __construct(ContaoFramework $framework, RequestStack $requestStack, ContaoCsrfTokenManager $csrfTokenManager) 51 | { 52 | $this->framework = $framework; 53 | $this->requestStack = $requestStack; 54 | $this->template = new BackendTemplate('be_sf_moderate'); 55 | $this->csrfTokenManager = $csrfTokenManager; 56 | } 57 | 58 | /** 59 | * Run the controller. 60 | * 61 | * @codeCoverageIgnore 62 | */ 63 | public function run(): string 64 | { 65 | $formId = 'tl_news_moderate'; 66 | 67 | $request = $this->requestStack->getCurrentRequest(); 68 | 69 | if ($request->request->get('FORM_SUBMIT') === $formId) { 70 | $this->processForm($request); 71 | } 72 | 73 | return $this->getTemplate($formId)->parse(); 74 | } 75 | 76 | /** 77 | * Generate the options. 78 | * 79 | * @codeCoverageIgnore 80 | */ 81 | public static function generateOptions(bool|string $filter = false): array 82 | { 83 | $options = []; 84 | 85 | if ($filter) { 86 | $objFeedModel = SocialFeedModel::findBy('socialFeedType', $filter); 87 | } else { 88 | $objFeedModel = SocialFeedModel::findAll(); 89 | } 90 | 91 | foreach ($objFeedModel as $feed) { 92 | $options[$feed->id] = $feed->socialFeedType.' '; 93 | 94 | if ('Facebook' === $feed->socialFeedType) { 95 | $options[$feed->id] .= $feed->pdir_sf_fb_account; 96 | } 97 | 98 | if ('Instagram' === $feed->socialFeedType) { 99 | $options[$feed->id] .= $feed->instagram_account; 100 | } 101 | 102 | if ('Twitter' === $feed->socialFeedType) { 103 | $options[$feed->id] .= $feed->twitter_account; 104 | } 105 | 106 | if ('LinkedIn' === $feed->socialFeedType) { 107 | $options[$feed->id] .= $feed->linkedin_account; 108 | } 109 | 110 | $options[$feed->id] .= ' (ID '.$feed->id.')'; 111 | } 112 | 113 | return $options; 114 | } 115 | 116 | public function shortenHeadline($item): void 117 | { 118 | $message = $item['headline'] ?? ''; 119 | $more = ''; 120 | 121 | if (\strlen($message) > 50) { 122 | $more = ' ...'; 123 | } 124 | 125 | $item['headline'] = mb_substr($message, 0, 50).$more; 126 | } 127 | 128 | public function getPostImage(SocialFeedModel $socialFeedAccount, $item) 129 | { 130 | $imgPath = NewsImporter::createImageFolder($socialFeedAccount->id); // create image folder 131 | 132 | if ('VIDEO' === $item['media_type'] || 'IMAGE' === $item['media_type'] || 'CAROUSEL_ALBUM' === $item['media_type']) { 133 | $imgSrc = ''; 134 | 135 | if (isset($item['media_url'])) { 136 | $imgSrc = false !== \strpos($item['media_url'], 'jpg') ? $item['media_url'] : $item['thumbnail_url']; 137 | } 138 | 139 | if (!isset($item['media_url']) && isset($item['children']['data'][0]['media_url'])) { 140 | $imgSrc = $item['children']['data'][0]['media_url']; 141 | } 142 | 143 | $picturePath = $imgPath.$item['id'].'.jpg'; 144 | 145 | return NewsImporter::saveImage($picturePath, $imgSrc); 146 | } 147 | } 148 | 149 | /** 150 | * Process the form. 151 | * 152 | * @codeCoverageIgnore 153 | */ 154 | protected function processForm(Request $request): void 155 | { 156 | if (!$request->request->get('account')) { 157 | return; 158 | } 159 | 160 | $socialFeedAccount = $request->request->get('account'); 161 | $objSocialFeedModel = SocialFeedModel::findById($socialFeedAccount); 162 | $newsArchiveId = Input::get('id'); 163 | 164 | $objImporter = new Importer(); 165 | $items = $objImporter->getPostsByAccount($request->request->get('account'), $request->request->get('number_posts')); 166 | 167 | // import selected items 168 | $allValues = $request->request->all(); 169 | 170 | // do import if importItems is set 171 | if (isset($allValues['importItems']) && \count($allValues['importItems']) > 0) { 172 | foreach ($items as $item) { 173 | if (\in_array($item['id'], $allValues['importItems'], true)) { 174 | $importer = new NewsImporter(); 175 | 176 | // set headline 177 | $item['headline'] = NewsImporter::shortenHeadline($item['caption']); 178 | 179 | // set teaser 180 | $item['teaser'] = str_replace("\n", '
', $item['caption']); 181 | 182 | // add image 183 | $item['singleSRC'] = $this->getPostImage($objSocialFeedModel, $item); 184 | 185 | $item['date'] = strtotime($item['timestamp']); 186 | $item['time'] = strtotime($item['timestamp']); 187 | 188 | $importer->setNews($item); 189 | $importer->accountImage = $objImporter->getAccountImage(); 190 | $importer->execute($newsArchiveId, $objSocialFeedModel); 191 | } 192 | } 193 | } 194 | 195 | // set import message 196 | if (\is_array($items) && isset($allValues['importItems']) && \count($allValues['importItems']) > 0) { 197 | $this->message = \sprintf($GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['importMessage'], \count($allValues['importItems'])); 198 | } 199 | 200 | if (null === $items) { 201 | Message::addInfo($GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['noItems']); 202 | } 203 | 204 | // get items for moderation list 205 | if (null !== $items) { 206 | $moderationItems = $objImporter->moderation($items); 207 | 208 | if (0 < \count($moderationItems)) { 209 | $template = new BackendTemplate('be_sf_moderation_list'); 210 | $template->arr = $moderationItems; 211 | $html = $template->parse(); 212 | } 213 | } 214 | 215 | $this->template->activeAccount = $request->request->get('account'); 216 | $this->template->moderationList = $html ?? ''; 217 | $this->template->message = isset($this->message) ? '
'.$this->message.'
' : ''; 218 | } 219 | 220 | /** 221 | * Get the template. 222 | * 223 | * @codeCoverageIgnore 224 | */ 225 | protected function getTemplate(string $formId): BackendTemplate 226 | { 227 | /** 228 | * @var Environment 229 | * @var Message $message 230 | * @var System $system 231 | */ 232 | $environment = $this->framework->getAdapter(Environment::class); 233 | $system = $this->framework->getAdapter(System::class); 234 | 235 | if (isset($this->message)) { 236 | Message::addInfo($this->message); 237 | } 238 | 239 | $this->template->backUrl = $system->getReferer(); 240 | $this->template->action = $environment->get('request'); 241 | $this->template->formId = $formId; 242 | $this->template->message = isset($this->message) ? '
'.$this->message.'
' : ''; 243 | $this->template->options = $this->generateOptions('Instagram'); 244 | $this->template->headline = $GLOBALS['TL_LANG']['BE_MOD']['socialFeedModerate']['headline'].Input::get('id'); 245 | $this->template->requestToken = $this->csrfTokenManager->getDefaultTokenValue(); 246 | 247 | return $this->template; 248 | } 249 | } 250 | --------------------------------------------------------------------------------